From 928d1e6935b85f0b043545520af88f153919524b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 21 Aug 2026 12:23:09 +0200 Subject: [PATCH 1/2] Reformat with Dart 3.10 --- packages/powersync/example/batch_writes.dart | 8 +- .../powersync/example/getting_started.dart | 7 +- .../powersync/example/watching_changes.dart | 49 +- packages/powersync/hook/build.dart | 67 ++- .../lib/src/attachments/attachment.dart | 34 +- .../attachments/attachment_queue_service.dart | 61 ++- .../implementations/attachment_context.dart | 14 +- .../lib/src/attachments/io_local_storage.dart | 10 +- .../lib/src/attachments/local_storage.dart | 5 +- .../lib/src/attachments/remote_storage.dart | 5 +- .../src/attachments/sync/syncing_service.dart | 63 ++- .../src/attachments/sync_error_handler.dart | 40 +- packages/powersync/lib/src/connector.dart | 98 ++-- packages/powersync/lib/src/crud.dart | 26 +- .../lib/src/database/active_instances.dart | 8 +- .../lib/src/database/core_version.dart | 20 +- .../lib/src/database/encryption_options.dart | 6 +- .../native/native_powersync_database.dart | 121 +++-- .../native/sync_isolate_protocol.dart | 21 +- .../lib/src/database/powersync_database.dart | 111 ++-- .../database/web/web_powersync_database.dart | 57 ++- .../powersync/lib/src/devtools/extension.dart | 44 +- .../powersync/lib/src/devtools/protocol.dart | 40 +- packages/powersync/lib/src/exceptions.dart | 10 +- .../powersync/lib/src/isolate_completer.dart | 20 +- packages/powersync/lib/src/log.dart | 3 +- .../native/native_open_factory.dart | 10 +- .../native/sqlite3_powersync_init.dart | 4 +- .../open_factory/web/web_open_factory.dart | 27 +- .../lib/src/platform_specific/native.dart | 10 +- .../src/platform_specific/unsupported.dart | 5 +- .../lib/src/platform_specific/web.dart | 10 +- .../src/powersync_update_notification.dart | 13 +- packages/powersync/lib/src/schema.dart | 101 ++-- packages/powersync/lib/src/setup/web.dart | 110 ++-- .../lib/src/sync/bucket_storage.dart | 47 +- .../lib/src/sync/connection_manager.dart | 62 ++- .../powersync/lib/src/sync/instruction.dart | 55 +- .../lib/src/sync/internal_connector.dart | 16 +- packages/powersync/lib/src/sync/options.dart | 17 +- packages/powersync/lib/src/sync/stream.dart | 20 +- .../powersync/lib/src/sync/stream_utils.dart | 40 +- .../lib/src/sync/streaming_sync.dart | 229 +++++---- .../powersync/lib/src/sync/sync_status.dart | 60 ++- .../powersync/lib/src/web/http/server.dart | 7 +- .../lib/src/web/sync_controller.dart | 11 +- .../powersync/lib/src/web/sync_worker.dart | 113 +++-- .../lib/src/web/sync_worker_protocol.dart | 271 +++++----- packages/powersync/lib/src/web/worker.dart | 18 +- .../powersync/lib/src/web/worker_utils.dart | 12 +- packages/powersync/pubspec.yaml | 2 +- .../test/attachments/attachment_test.dart | 125 +++-- .../test/attachments/local_storage_test.dart | 85 ++-- packages/powersync/test/connected_test.dart | 216 ++++---- packages/powersync/test/credentials_test.dart | 6 +- packages/powersync/test/crud_test.dart | 478 +++++++++++------- .../test/database/core_version_test.dart | 18 +- .../test/database/encryption_test.dart | 47 +- packages/powersync/test/devtools/app.dart | 2 +- .../test/devtools/devtools_test.dart | 21 +- packages/powersync/test/disconnect_test.dart | 14 +- packages/powersync/test/exceptions_test.dart | 43 +- .../powersync/test/offline_online_test.dart | 140 +++-- .../test/performance_native_test.dart | 130 +++-- .../test/performance_shared_test.dart | 69 ++- .../powersync/test/powersync_native_test.dart | 118 +++-- .../powersync/test/powersync_shared_test.dart | 190 ++++--- packages/powersync/test/schema_test.dart | 329 +++++++----- .../powersync/test/server/asset_server.dart | 4 +- .../sync_server/in_memory_sync_server.dart | 36 +- .../powersync/test/server/worker_server.dart | 6 +- packages/powersync/test/stream_test.dart | 132 +++-- .../test/sync/custom_client_test.dart | 8 +- .../test/sync/in_memory_sync_test.dart | 427 ++++++++-------- .../powersync/test/sync/options_test.dart | 40 +- packages/powersync/test/sync/protocol.dart | 167 +++--- packages/powersync/test/sync/stream_test.dart | 92 ++-- .../test/sync/streaming_sync_test.dart | 54 +- .../powersync/test/sync/sync_status_test.dart | 49 +- packages/powersync/test/sync/utils.dart | 33 +- packages/powersync/test/test_server.dart | 13 +- packages/powersync/test/upload_test.dart | 28 +- .../test/utils/abstract_test_utils.dart | 88 ++-- .../powersync/test/utils/in_memory_http.dart | 49 +- .../powersync/test/utils/stub_test_utils.dart | 11 +- .../powersync/test/utils/web_test_utils.dart | 11 +- packages/powersync/test/version_test.dart | 9 +- packages/powersync/test/watch_test.dart | 114 +++-- packages/powersync/test/web/http_test.dart | 121 +++-- .../powersync/test/web/sync_worker_test.dart | 48 +- .../tool/update_core_extension_hashes.dart | 29 +- 91 files changed, 3407 insertions(+), 2411 deletions(-) diff --git a/packages/powersync/example/batch_writes.dart b/packages/powersync/example/batch_writes.dart index 34d064c7..6299a883 100644 --- a/packages/powersync/example/batch_writes.dart +++ b/packages/powersync/example/batch_writes.dart @@ -4,7 +4,7 @@ import 'package:powersync/powersync.dart'; late PowerSyncDatabase db; const schema = Schema([ - Table.localOnly('data', [Column.text('contents')]) + Table.localOnly('data', [Column.text('contents')]), ]); final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']); @@ -37,7 +37,9 @@ Future batchWrites() async { // This avoids the overhead of asynchronously waiting for each call to complete, // and also only parses the SQL statement once. await db.executeBatch( - 'INSERT INTO data(id, contents) VALUES(?, ?)', parameterSets); + 'INSERT INTO data(id, contents) VALUES(?, ?)', + parameterSets, + ); } Future inIsolateWrites() async { @@ -66,7 +68,7 @@ Future main() async { singleWrites, transactionalWrites, batchWrites, - inIsolateWrites + inIsolateWrites, ]) { await db.execute('DELETE FROM data WHERE 1'); var watch = Stopwatch()..start(); diff --git a/packages/powersync/example/getting_started.dart b/packages/powersync/example/getting_started.dart index a2eec697..718f3cd1 100644 --- a/packages/powersync/example/getting_started.dart +++ b/packages/powersync/example/getting_started.dart @@ -1,7 +1,7 @@ import 'package:powersync/powersync.dart'; const schema = Schema([ - Table('customers', [Column.text('name'), Column.text('email')]) + Table('customers', [Column.text('name'), Column.text('email')]), ]); late PowerSyncDatabase db; @@ -38,8 +38,9 @@ Future openDatabase() async { // Run local statements. await db.execute( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Fred', 'fred@example.org']); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Fred', 'fred@example.org'], + ); // Connect to backend db.connect(connector: BackendConnector(db)); diff --git a/packages/powersync/example/watching_changes.dart b/packages/powersync/example/watching_changes.dart index 74a9f34b..d07fc06f 100644 --- a/packages/powersync/example/watching_changes.dart +++ b/packages/powersync/example/watching_changes.dart @@ -5,7 +5,7 @@ import 'package:powersync/powersync.dart'; late PowerSyncDatabase db; const schema = Schema([ - Table.localOnly('data', [Column.text('contents')]) + Table.localOnly('data', [Column.text('contents')]), ]); final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']); @@ -20,30 +20,43 @@ Future main() async { // Watch a single query. // The query is executed every time one of its source tables are changed. - var subscription1 = - db.watch('SELECT count() AS count FROM data').listen((results) { - print('Results: $results'); - }, onError: (Object e) { - print('Query failed: $e'); - }); + var subscription1 = db + .watch('SELECT count() AS count FROM data') + .listen( + (results) { + print('Results: $results'); + }, + onError: (Object e) { + print('Query failed: $e'); + }, + ); // Watch for changes to one or more tables. // For this form, the tables to watch must be manually specified. // Use asyncMap here to avoid the event being triggered while previous queries // are still running. - var subscription2 = db.onChange(['data']).asyncMap((update) async { - var count = await db.get('SELECT count() AS count FROM data'); - var length = - await db.get('SELECT sum(length(contents)) AS length FROM data'); - print( - 'Results after change to ${update.tables}: ${count['count']} entries, ${length['length']} characters'); - }).listen((_) {}, onError: (Object e) { - print('Query failed: $e'); - }); + var subscription2 = db + .onChange(['data']) + .asyncMap((update) async { + var count = await db.get('SELECT count() AS count FROM data'); + var length = await db.get( + 'SELECT sum(length(contents)) AS length FROM data', + ); + print( + 'Results after change to ${update.tables}: ${count['count']} entries, ${length['length']} characters', + ); + }) + .listen( + (_) {}, + onError: (Object e) { + print('Query failed: $e'); + }, + ); for (var i = 0; i < 10; i++) { - await db.execute( - 'INSERT INTO data(id, contents) VALUES(uuid(), ?)', ['Row $i']); + await db.execute('INSERT INTO data(id, contents) VALUES(uuid(), ?)', [ + 'Row $i', + ]); await Future.delayed(Duration(milliseconds: 500)); } diff --git a/packages/powersync/hook/build.dart b/packages/powersync/hook/build.dart index e7acf854..95e85289 100644 --- a/packages/powersync/hook/build.dart +++ b/packages/powersync/hook/build.dart @@ -34,12 +34,14 @@ void main(List args) async { coreExtension = await _reuseOrDownloadCoreExtension(input); } - output.assets.code.add(CodeAsset( - package: 'powersync', - name: 'src/open_factory/native/sqlite3_powersync_init.dart', - linkMode: DynamicLoadingBundled(), - file: coreExtension.uri, - )); + output.assets.code.add( + CodeAsset( + package: 'powersync', + name: 'src/open_factory/native/sqlite3_powersync_init.dart', + linkMode: DynamicLoadingBundled(), + file: coreExtension.uri, + ), + ); }); } @@ -47,18 +49,23 @@ Future _reuseOrDownloadCoreExtension(BuildInput input) async { final sourceFileName = _fileNameForBuild(input.config.code); final digest = assetNameToSha256Hash[sourceFileName]!; - final targetUri = input.outputDirectoryShared - .resolve('download-${digest.substring(0, 8)}/'); + final targetUri = input.outputDirectoryShared.resolve( + 'download-${digest.substring(0, 8)}/', + ); final targetDirectory = Directory(targetUri.toFilePath()); if (!targetDirectory.existsSync()) { targetDirectory.createSync(); } - final file = File(targetUri - .resolve(input.config.code.targetOS.libraryFileName( - 'powersync_core', - DynamicLoadingBundled(), - )) - .toFilePath()); + final file = File( + targetUri + .resolve( + input.config.code.targetOS.libraryFileName( + 'powersync_core', + DynamicLoadingBundled(), + ), + ) + .toFilePath(), + ); if (file.existsSync()) { // Hook is re-run with an existing cache. Does the file match the digest we @@ -78,11 +85,13 @@ Future _reuseOrDownloadCoreExtension(BuildInput input) async { } Future _fetchCoreExtension(String fileName, String hash) async { - final client = IOClient(HttpClient() - // From Dart 3.11, proxy-related environment variables are passed to - // hooks. We respect them to ensure we can download these binaries in - // environments where that's required. - ..findProxy = HttpClient.findProxyFromEnvironment); + final client = IOClient( + HttpClient() + // From Dart 3.11, proxy-related environment variables are passed to + // hooks. We respect them to ensure we can download these binaries in + // environments where that's required. + ..findProxy = HttpClient.findProxyFromEnvironment, + ); final uri = Uri.https( 'github.com', 'powersync-ja/powersync-sqlite-core/releases/download/$releaseVersion/$fileName', @@ -92,13 +101,15 @@ Future _fetchCoreExtension(String fileName, String hash) async { final response = await client.get(uri); if (response.statusCode != 200) { throw Exception( - 'Could not download $uri, got ${response.statusCode}: ${response.body}'); + 'Could not download $uri, got ${response.statusCode}: ${response.body}', + ); } final digest = sha256.convert(response.bodyBytes); if (digest.toString() != hash) { throw Exception( - 'Unexpected digest for $uri, expected $hash got $digest.'); + 'Unexpected digest for $uri, expected $hash got $digest.', + ); } return response.bodyBytes; @@ -153,7 +164,10 @@ String _fileNameForBuild(CodeConfig config) { return 'libpowersync_${architectureName()}.ios-sim.dylib'; case OS.linux: final archName = architectureName( - supportArmv7: true, supportX86: true, supportRiscv: true); + supportArmv7: true, + supportX86: true, + supportRiscv: true, + ); return 'libpowersync_$archName.linux.so'; case OS.macOS: return 'libpowersync_${architectureName()}.macos.dylib'; @@ -169,7 +183,10 @@ String _fileNameForBuild(CodeConfig config) { } Future _useLocalCoreExtension( - String root, BuildInput input, BuildOutputBuilder output) async { + String root, + BuildInput input, + BuildOutputBuilder output, +) async { final config = input.config.code; if (config.targetOS != OS.current || config.targetArchitecture != Architecture.current) { @@ -193,9 +210,7 @@ Future _useLocalCoreExtension( final rootUri = Uri.directory(root); final library = rootUri.resolve('target/debug/$outputName'); final depsFile = File.fromUri( - library.resolve( - Platform.isWindows ? 'powersync.d' : 'libpowersync.d', - ), + library.resolve(Platform.isWindows ? 'powersync.d' : 'libpowersync.d'), ); // Parse generated depfile to re-run this hook when a Rust source has changed. diff --git a/packages/powersync/lib/src/attachments/attachment.dart b/packages/powersync/lib/src/attachments/attachment.dart index d730eb2b..9502de29 100644 --- a/packages/powersync/lib/src/attachments/attachment.dart +++ b/packages/powersync/lib/src/attachments/attachment.dart @@ -177,22 +177,24 @@ extension type AttachmentsQueueTable._(Table _) implements Table { List indexes = const [], String? viewName, }) { - return AttachmentsQueueTable._(Table.localOnly( - attachmentsQueueTableName, - [ - const Column.text('filename'), - const Column.text('local_uri'), - const Column.integer('timestamp'), - const Column.integer('size'), - const Column.text('media_type'), - const Column.integer('state'), - const Column.integer('has_synced'), - const Column.text('meta_data'), - ...additionalColumns, - ], - viewName: viewName, - indexes: indexes, - )); + return AttachmentsQueueTable._( + Table.localOnly( + attachmentsQueueTableName, + [ + const Column.text('filename'), + const Column.text('local_uri'), + const Column.integer('timestamp'), + const Column.integer('size'), + const Column.text('media_type'), + const Column.integer('state'), + const Column.integer('has_synced'), + const Column.text('meta_data'), + ...additionalColumns, + ], + viewName: viewName, + indexes: indexes, + ), + ); } static const defaultTableName = 'attachments_queue'; diff --git a/packages/powersync/lib/src/attachments/attachment_queue_service.dart b/packages/powersync/lib/src/attachments/attachment_queue_service.dart index 20ccbe2d..d39d94f3 100644 --- a/packages/powersync/lib/src/attachments/attachment_queue_service.dart +++ b/packages/powersync/lib/src/attachments/attachment_queue_service.dart @@ -52,9 +52,9 @@ final class WatchedAttachmentItem { this.filename, this.metaData, }) : assert( - fileExtension != null || filename != null, - 'Either fileExtension or filename must be provided.', - ); + fileExtension != null || filename != null, + 'Either fileExtension or filename must be provided.', + ); } /// Class used to implement the attachment queue. @@ -78,21 +78,21 @@ base class AttachmentQueue { final AttachmentService _attachmentsService; final SyncingService _syncingService; - AttachmentQueue._( - {required PowerSyncDatabase db, - required Stream> Function() watchAttachments, - required LocalStorage localStorage, - required bool downloadAttachments, - required Logger logger, - required AttachmentService attachmentsService, - required SyncingService syncingService}) - : _db = db, - _watchAttachments = watchAttachments, - _localStorage = localStorage, - _downloadAttachments = downloadAttachments, - _logger = logger, - _attachmentsService = attachmentsService, - _syncingService = syncingService; + AttachmentQueue._({ + required PowerSyncDatabase db, + required Stream> Function() watchAttachments, + required LocalStorage localStorage, + required bool downloadAttachments, + required Logger logger, + required AttachmentService attachmentsService, + required SyncingService syncingService, + }) : _db = db, + _watchAttachments = watchAttachments, + _localStorage = localStorage, + _downloadAttachments = downloadAttachments, + _logger = logger, + _attachmentsService = attachmentsService, + _syncingService = syncingService; /// Creates a new attachment queue. /// @@ -172,8 +172,9 @@ base class AttachmentQueue { // Listen for connectivity changes and watched attachments await _syncingService.startSync(); - _watchedAttachmentsSubscription = - _watchAttachments().listen((items) async { + _watchedAttachmentsSubscription = _watchAttachments().listen(( + items, + ) async { await _processWatchedAttachments(items); }); @@ -249,15 +250,17 @@ base class AttachmentQueue { final List attachmentUpdates = []; for (final item in items) { - final existingQueueItem = - currentAttachments.where((a) => a.id == item.id).firstOrNull; + final existingQueueItem = currentAttachments + .where((a) => a.id == item.id) + .firstOrNull; if (existingQueueItem == null) { if (!_downloadAttachments) continue; // This item should be added to the queue. // This item is assumed to be coming from an upstream sync. - final String filename = item.filename ?? + final String filename = + item.filename ?? await resolveNewAttachmentFilename(item.id, item.fileExtension); attachmentUpdates.add( @@ -334,8 +337,10 @@ base class AttachmentQueue { String? metaData, String? id, required Future Function( - SqliteWriteContext context, Attachment attachment) - updateHook, + SqliteWriteContext context, + Attachment attachment, + ) + updateHook, }) async { final resolvedId = id ?? await generateAttachmentId(); @@ -372,8 +377,10 @@ base class AttachmentQueue { Future deleteFile({ required String attachmentId, required Future Function( - SqliteWriteContext context, Attachment attachment) - updateHook, + SqliteWriteContext context, + Attachment attachment, + ) + updateHook, }) async { return await _attachmentsService.withContext((attachmentContext) async { final attachment = await attachmentContext.getAttachment(attachmentId); diff --git a/packages/powersync/lib/src/attachments/implementations/attachment_context.dart b/packages/powersync/lib/src/attachments/implementations/attachment_context.dart index 0473066f..62510bfe 100644 --- a/packages/powersync/lib/src/attachments/implementations/attachment_context.dart +++ b/packages/powersync/lib/src/attachments/implementations/attachment_context.dart @@ -101,21 +101,19 @@ final class AttachmentContext { final results = await db.getAll( 'SELECT * FROM $table WHERE state = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?', - [ - AttachmentState.archived.index, - limit, - maxArchivedCount, - ], + [AttachmentState.archived.index, limit, maxArchivedCount], ); - final archivedAttachments = - results.map((row) => Attachment.fromRow(row)).toList(); + final archivedAttachments = results + .map((row) => Attachment.fromRow(row)) + .toList(); if (archivedAttachments.isEmpty) { return false; } log.info( - 'Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...'); + 'Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...', + ); // Call the callback with the list of archived attachments before deletion await callback(archivedAttachments); diff --git a/packages/powersync/lib/src/attachments/io_local_storage.dart b/packages/powersync/lib/src/attachments/io_local_storage.dart index 67d1b578..646013dd 100644 --- a/packages/powersync/lib/src/attachments/io_local_storage.dart +++ b/packages/powersync/lib/src/attachments/io_local_storage.dart @@ -79,10 +79,12 @@ final class _LengthTrackingSink implements StreamConsumer> { @override Future addStream(Stream> stream) { - return inner.addStream(stream.map((event) { - bytesWritten += event.length; - return event; - })); + return inner.addStream( + stream.map((event) { + bytesWritten += event.length; + return event; + }), + ); } @override diff --git a/packages/powersync/lib/src/attachments/local_storage.dart b/packages/powersync/lib/src/attachments/local_storage.dart index f21bf4a6..67474362 100644 --- a/packages/powersync/lib/src/attachments/local_storage.dart +++ b/packages/powersync/lib/src/attachments/local_storage.dart @@ -81,8 +81,9 @@ final class _InMemoryStorage implements LocalStorage { @override Stream readFile(String filePath) { return switch (content[_keyForPath(filePath)]) { - null => - Stream.error('file at $filePath does not exist in in-memory storage'), + null => Stream.error( + 'file at $filePath does not exist in in-memory storage', + ), final contents => Stream.value(contents), }; } diff --git a/packages/powersync/lib/src/attachments/remote_storage.dart b/packages/powersync/lib/src/attachments/remote_storage.dart index 35818b9b..19b7736e 100644 --- a/packages/powersync/lib/src/attachments/remote_storage.dart +++ b/packages/powersync/lib/src/attachments/remote_storage.dart @@ -15,10 +15,7 @@ abstract interface class RemoteStorage { /// /// [fileData] is a stream of byte arrays representing the file data. /// [attachment] is the attachment record associated with the file. - Future uploadFile( - Stream fileData, - Attachment attachment, - ); + Future uploadFile(Stream fileData, Attachment attachment); /// Downloads a file from remote storage. /// diff --git a/packages/powersync/lib/src/attachments/sync/syncing_service.dart b/packages/powersync/lib/src/attachments/sync/syncing_service.dart index ba61354d..ca64d5a2 100644 --- a/packages/powersync/lib/src/attachments/sync/syncing_service.dart +++ b/packages/powersync/lib/src/attachments/sync/syncing_service.dart @@ -62,32 +62,31 @@ final class SyncingService { late StreamSubscription sub; final syncStream = - StreamGroup.merge([attachmentChanges, manualTriggers]) - .takeWhile((_) => sub == _syncSubscription) - .asyncMap((_) async { - // Read the active set inside the mutex; release it before doing any - // per-attachment I/O so that concurrent `saveFile`/`deleteFile` and - // `_processWatchedAttachments` calls aren't blocked for the duration - // of the whole batch. - final attachments = await attachmentsService.withContext( - (context) => context.getActiveAttachments(), - ); - logger.finest('Found ${attachments.length} active attachments'); + StreamGroup.merge([ + attachmentChanges, + manualTriggers, + ]).takeWhile((_) => sub == _syncSubscription).asyncMap((_) async { + // Read the active set inside the mutex; release it before doing any + // per-attachment I/O so that concurrent `saveFile`/`deleteFile` and + // `_processWatchedAttachments` calls aren't blocked for the duration + // of the whole batch. + final attachments = await attachmentsService.withContext( + (context) => context.getActiveAttachments(), + ); + logger.finest('Found ${attachments.length} active attachments'); - await _handleSync( - attachments, - isActive: () => sub == _syncSubscription, - ); + await _handleSync( + attachments, + isActive: () => sub == _syncSubscription, + ); - await attachmentsService.withContext(deleteArchivedAttachments); - }); + await attachmentsService.withContext(deleteArchivedAttachments); + }); _syncSubscription = sub = syncStream.listen(null); // Start periodic sync using instance period - _periodicSubscription = Stream.periodic(period, (_) {}).listen(( - _, - ) { + _periodicSubscription = Stream.periodic(period, (_) {}).listen((_) { logger.finer('Periodically syncing attachments (interval $period)'); triggerSync(); }); @@ -183,7 +182,8 @@ final class SyncingService { /// Returns the updated attachment with its new state. Future _uploadAttachment(Attachment attachment) async { logger.info( - 'Starting upload for attachment ${attachment.id} (local filename ${attachment.filename})'); + 'Starting upload for attachment ${attachment.id} (local filename ${attachment.filename})', + ); try { if (attachment.localUri == null) { throw Exception('No localUri for attachment $attachment'); @@ -236,15 +236,14 @@ final class SyncingService { hasSynced: true, ); } catch (e, st) { - logger.warning( - 'Download error for attachment $attachment', - e, - st, - ); + logger.warning('Download error for attachment $attachment', e, st); if (errorHandler case final errorHandler?) { - final shouldRetry = - await errorHandler.onDownloadError(attachment, e, st); + final shouldRetry = await errorHandler.onDownloadError( + attachment, + e, + st, + ); if (!shouldRetry) { logger.warning( 'Attachment with ID ${attachment.id} has been archived after download error', @@ -262,7 +261,9 @@ final class SyncingService { /// [attachment]: The attachment to delete. /// Returns the updated attachment with its new state. Future deleteAttachment( - Attachment attachment, AttachmentContext context) async { + Attachment attachment, + AttachmentContext context, + ) async { try { logger.info('Deleting attachment ${attachment.id} from remote storage'); await remoteStorage.deleteFile(attachment); @@ -294,9 +295,7 @@ final class SyncingService { /// /// [context]: The attachment context used to retrieve and manage archived attachments. /// Returns `true` if all archived attachments were successfully deleted, `false` otherwise. - Future deleteArchivedAttachments( - AttachmentContext context, - ) async { + Future deleteArchivedAttachments(AttachmentContext context) async { return context.deleteArchivedAttachments((pendingDelete) async { for (final attachment in pendingDelete) { if (attachment.localUri == null) continue; diff --git a/packages/powersync/lib/src/attachments/sync_error_handler.dart b/packages/powersync/lib/src/attachments/sync_error_handler.dart index 30aafcf4..ca16743f 100644 --- a/packages/powersync/lib/src/attachments/sync_error_handler.dart +++ b/packages/powersync/lib/src/attachments/sync_error_handler.dart @@ -8,11 +8,12 @@ import 'attachment.dart'; /// It returns `true` if the operation should be retried. /// /// {@category attachments} -typedef AttachmentExceptionHandler = Future Function( - Attachment attachment, - Object exception, - StackTrace stackTrace, -); +typedef AttachmentExceptionHandler = + Future Function( + Attachment attachment, + Object exception, + StackTrace stackTrace, + ); /// Interface for handling errors during attachment operations. /// Implementations determine whether failed operations should be retried. @@ -74,29 +75,38 @@ final class _FunctionBasedErrorHandler implements AttachmentErrorHandler { final AttachmentExceptionHandler _onDownloadError; final AttachmentExceptionHandler _onUploadError; - const _FunctionBasedErrorHandler( - {required AttachmentExceptionHandler onDeleteError, - required AttachmentExceptionHandler onDownloadError, - required AttachmentExceptionHandler onUploadError}) - : _onDeleteError = onDeleteError, - _onDownloadError = onDownloadError, - _onUploadError = onUploadError; + const _FunctionBasedErrorHandler({ + required AttachmentExceptionHandler onDeleteError, + required AttachmentExceptionHandler onDownloadError, + required AttachmentExceptionHandler onUploadError, + }) : _onDeleteError = onDeleteError, + _onDownloadError = onDownloadError, + _onUploadError = onUploadError; @override Future onDeleteError( - Attachment attachment, Object exception, StackTrace stackTrace) { + Attachment attachment, + Object exception, + StackTrace stackTrace, + ) { return _onDeleteError(attachment, exception, stackTrace); } @override Future onDownloadError( - Attachment attachment, Object exception, StackTrace stackTrace) { + Attachment attachment, + Object exception, + StackTrace stackTrace, + ) { return _onDownloadError(attachment, exception, stackTrace); } @override Future onUploadError( - Attachment attachment, Object exception, StackTrace stackTrace) { + Attachment attachment, + Object exception, + StackTrace stackTrace, + ) { return _onUploadError(attachment, exception, stackTrace); } } diff --git a/packages/powersync/lib/src/connector.dart b/packages/powersync/lib/src/connector.dart index acb7b961..02c8bf08 100644 --- a/packages/powersync/lib/src/connector.dart +++ b/packages/powersync/lib/src/connector.dart @@ -38,12 +38,14 @@ abstract class PowerSyncBackendConnector { /// /// This may be called before the current credentials have expired. Future prefetchCredentials() async { - _fetchRequest ??= fetchCredentials().then((value) { - _cachedCredentials = value; - return value; - }).whenComplete(() { - _fetchRequest = null; - }); + _fetchRequest ??= fetchCredentials() + .then((value) { + _cachedCredentials = value; + return value; + }) + .whenComplete(() { + _fetchRequest = null; + }); return _fetchRequest!; } @@ -81,11 +83,12 @@ class PowerSyncCredentials { /// When the token expires. Only use for debugging purposes. final DateTime? expiresAt; - PowerSyncCredentials( - {required this.endpoint, - required this.token, - this.userId, - this.expiresAt}) { + PowerSyncCredentials({ + required this.endpoint, + required this.token, + this.userId, + this.expiresAt, + }) { _validateEndpoint(); } @@ -94,10 +97,11 @@ class PowerSyncCredentials { DateTime? expiresAt = getExpiryDate(token); return PowerSyncCredentials( - endpoint: parsed['endpoint'] as String, - token: token, - userId: parsed['user_id'] as String?, - expiresAt: expiresAt); + endpoint: parsed['endpoint'] as String, + token: token, + userId: parsed['user_id'] as String?, + expiresAt: expiresAt, + ); } /// Get an expiry date from a JWT token, if specified. @@ -136,7 +140,9 @@ class PowerSyncCredentials { if ((!parsed.isScheme('http') && !parsed.isScheme('https')) || parsed.host.isEmpty) { throw ArgumentError.value( - endpoint, 'PowerSync endpoint must be a valid URL'); + endpoint, + 'PowerSync endpoint must be a valid URL', + ); } } } @@ -162,9 +168,10 @@ class DevCredentials { factory DevCredentials.fromJson(Map parsed) { return DevCredentials( - endpoint: parsed['endpoint'] as String, - token: parsed['token'] as String?, - userId: parsed['user_id'] as String?); + endpoint: parsed['endpoint'] as String, + token: parsed['token'] as String?, + userId: parsed['user_id'] as String?, + ); } factory DevCredentials.fromString(String credentials) { @@ -244,23 +251,29 @@ class DevConnector extends PowerSyncBackendConnector { } /// Use the PowerSync dev API to log in. - Future devLogin( - {required String endpoint, - required String user, - required String password}) async { + Future devLogin({ + required String endpoint, + required String user, + required String password, + }) async { final uri = Uri.parse(endpoint).resolve('dev/auth.json'); - final res = await http.post(uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'user': user, 'password': password})); + final res = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user': user, 'password': password}), + ); if (res.statusCode == 200) { var parsed = jsonDecode(res.body); var data = parsed['data'] as Map; - storeDevCredentials(DevCredentials( + storeDevCredentials( + DevCredentials( endpoint: endpoint, token: data['token'] as String?, - userId: data['user_id'] as String?)); + userId: data['user_id'] as String?, + ), + ); } else { throw http.ClientException(res.reasonPhrase ?? 'Request failed', uri); } @@ -274,8 +287,10 @@ class DevConnector extends PowerSyncBackendConnector { return null; } final uri = Uri.parse(devCredentials!.endpoint).resolve('dev/token.json'); - final res = await http - .post(uri, headers: {'Authorization': 'Token ${devCredentials.token}'}); + final res = await http.post( + uri, + headers: {'Authorization': 'Token ${devCredentials.token}'}, + ); if (res.statusCode == 401) { clearDevToken(); } @@ -284,7 +299,8 @@ class DevConnector extends PowerSyncBackendConnector { } return PowerSyncCredentials.fromJson( - jsonDecode(res.body)['data'] as Map); + jsonDecode(res.body)['data'] as Map, + ); } /// Upload changes using the PowerSync dev API. @@ -301,13 +317,15 @@ class DevConnector extends PowerSyncBackendConnector { } final uri = credentials.endpointUri('crud.json'); - final response = await http.post(uri, - headers: { - 'Content-Type': 'application/json', - 'User-Id': credentials.userId ?? '', - 'Authorization': "Token ${credentials.token}" - }, - body: jsonEncode({'data': batch.crud, 'write_checkpoint': true})); + final response = await http.post( + uri, + headers: { + 'Content-Type': 'application/json', + 'User-Id': credentials.userId ?? '', + 'Authorization': "Token ${credentials.token}", + }, + body: jsonEncode({'data': batch.crud, 'write_checkpoint': true}), + ); if (response.statusCode == 401) { // Credentials have expired - fetch a new token on the next call @@ -316,7 +334,9 @@ class DevConnector extends PowerSyncBackendConnector { if (response.statusCode != 200) { throw http.ClientException( - response.reasonPhrase ?? "Failed due to server error.", uri); + response.reasonPhrase ?? "Failed due to server error.", + uri, + ); } final body = jsonDecode(response.body); diff --git a/packages/powersync/lib/src/crud.dart b/packages/powersync/lib/src/crud.dart index 8d84344c..b5f7f877 100644 --- a/packages/powersync/lib/src/crud.dart +++ b/packages/powersync/lib/src/crud.dart @@ -16,8 +16,11 @@ class CrudBatch { /// Call to remove the changes from the local queue, once successfully uploaded. Future Function({String? writeCheckpoint}) complete; - CrudBatch( - {required this.crud, required this.haveMore, required this.complete}); + CrudBatch({ + required this.crud, + required this.haveMore, + required this.complete, + }); } class CrudTransaction { @@ -32,10 +35,11 @@ class CrudTransaction { /// Call to remove the changes from the local queue, once successfully uploaded. final Future Function({String? writeCheckpoint}) complete; - CrudTransaction( - {required this.crud, - required this.complete, - required this.transactionId}); + CrudTransaction({ + required this.crud, + required this.complete, + required this.transactionId, + }); @override String toString() { @@ -148,8 +152,14 @@ class CrudEntry { @override int get hashCode { - return Object.hash(transactionId, clientId, op.toJson(), table, id, - const MapEquality().hash(opData)); + return Object.hash( + transactionId, + clientId, + op.toJson(), + table, + id, + const MapEquality().hash(opData), + ); } } diff --git a/packages/powersync/lib/src/database/active_instances.dart b/packages/powersync/lib/src/database/active_instances.dart index f352faae..60b504a4 100644 --- a/packages/powersync/lib/src/database/active_instances.dart +++ b/packages/powersync/lib/src/database/active_instances.dart @@ -26,8 +26,8 @@ final class ActiveDatabaseGroup { final String identifier; ActiveDatabaseGroup._(this.identifier) - : syncMutex = potentiallySharedMutex('$identifier-sync'), - crudMutex = potentiallySharedMutex('$identifier-crud'); + : syncMutex = potentiallySharedMutex('$identifier-sync'), + crudMutex = potentiallySharedMutex('$identifier-crud'); Future close() async { if (--refCount == 0) { @@ -40,7 +40,9 @@ final class ActiveDatabaseGroup { static ActiveDatabaseGroup referenceDatabase(String identifier) { final group = _activeGroups.putIfAbsent( - identifier, () => ActiveDatabaseGroup._(identifier)); + identifier, + () => ActiveDatabaseGroup._(identifier), + ); group.refCount++; return group; } diff --git a/packages/powersync/lib/src/database/core_version.dart b/packages/powersync/lib/src/database/core_version.dart index dbef44e1..6ddffe89 100644 --- a/packages/powersync/lib/src/database/core_version.dart +++ b/packages/powersync/lib/src/database/core_version.dart @@ -10,9 +10,9 @@ extension type const PowerSyncCoreVersion((int, int, int) _tuple) { int compareTo(PowerSyncCoreVersion other) { return switch (major.compareTo(other.major)) { 0 => switch (minor.compareTo(other.minor)) { - 0 => patch.compareTo(other.patch), - var other => other, - }, + 0 => patch.compareTo(other.patch), + var other => other, + }, var other => other, }; } @@ -44,15 +44,19 @@ extension type const PowerSyncCoreVersion((int, int, int) _tuple) { /// a [PowerSyncCoreVersion]. static PowerSyncCoreVersion parse(String version) { try { - final [major, minor, patch] = - version.split(RegExp(r'[./]')).take(3).map(int.parse).toList(); + final [major, minor, patch] = version + .split(RegExp(r'[./]')) + .take(3) + .map(int.parse) + .toList(); return PowerSyncCoreVersion((major, minor, patch)); } catch (e) { throw SqliteException( - extendedResultCode: 1, - message: - 'Unsupported powersync extension version. Need >=$minimum <$maximumExclusive, got: $version. Details: $e'); + extendedResultCode: 1, + message: + 'Unsupported powersync extension version. Need >=$minimum <$maximumExclusive, got: $version. Details: $e', + ); } } diff --git a/packages/powersync/lib/src/database/encryption_options.dart b/packages/powersync/lib/src/database/encryption_options.dart index 6596cbe1..d05372b7 100644 --- a/packages/powersync/lib/src/database/encryption_options.dart +++ b/packages/powersync/lib/src/database/encryption_options.dart @@ -78,8 +78,10 @@ final class EncryptionOptions { /// Throws if the `cipher` pragma doesn't exist, as that indicates that /// SQLite3MultipleCiphers is not available. - @Deprecated('Unused in PowerSync SDK, check ' - 'EncryptedSqliteVariant.resolveOnDatabase instead') + @Deprecated( + 'Unused in PowerSync SDK, check ' + 'EncryptedSqliteVariant.resolveOnDatabase instead', + ) static void checkHasCipherPragma(CommonDatabase database) { if (database.select('pragma cipher').isEmpty) { throw UnsupportedError( diff --git a/packages/powersync/lib/src/database/native/native_powersync_database.dart b/packages/powersync/lib/src/database/native/native_powersync_database.dart index d17cfc9f..78f92d13 100644 --- a/packages/powersync/lib/src/database/native/native_powersync_database.dart +++ b/packages/powersync/lib/src/database/native/native_powersync_database.dart @@ -125,10 +125,12 @@ final class NativePowerSyncDatabase extends BasePowerSyncDatabase { }; if (recoveredUploadError != null || recoveredDownloadError != null) { - setStatus(payload.changeErrors( - uploadError: recoveredUploadError ?? payload.uploadError, - downloadError: recoveredDownloadError ?? payload.downloadError, - )); + setStatus( + payload.changeErrors( + uploadError: recoveredUploadError ?? payload.uploadError, + downloadError: recoveredDownloadError ?? payload.downloadError, + ), + ); } else { setStatus(payload); } @@ -136,7 +138,11 @@ final class NativePowerSyncDatabase extends BasePowerSyncDatabase { case SyncIsolateToClientMessageType.log: LogRecord record = payload as LogRecord; logger.log( - record.level, record.message, record.error, record.stackTrace); + record.level, + record.message, + record.error, + record.stackTrace, + ); case SyncIsolateToClientMessageType.mutexAcquire: final (name, id) = payload as (String, int); mutexServer.acquireRequest(initPort!, name, id); @@ -196,8 +202,12 @@ final class NativePowerSyncDatabase extends BasePowerSyncDatabase { await hasInitPort.future; // Automatically complete the abort controller once the isolate exits. - unawaited(Future.any([abort.onAbort, receivedIsolateExit.future]) - .whenComplete(close)); + unawaited( + Future.any([ + abort.onAbort, + receivedIsolateExit.future, + ]).whenComplete(close), + ); } } @@ -232,13 +242,15 @@ Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { Future shutdown() { if (!shutdownCompleter.isCompleted) { - shutdownCompleter.complete(Future(() async { - await openedStreamingSync?.abort(); - await database.close(); - - rPort.close(); - results.close(); - })); + shutdownCompleter.complete( + Future(() async { + await openedStreamingSync?.abort(); + await database.close(); + + rPort.close(); + results.close(); + }), + ); } return shutdownCompleter.future; @@ -250,8 +262,9 @@ Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { case ClientToSyncIsolateMessageType.close: shutdown(); case ClientToSyncIsolateMessageType.changedSubscriptions: - openedStreamingSync - ?.updateSubscriptions(payload as List); + openedStreamingSync?.updateSubscriptions( + payload as List, + ); case ClientToSyncIsolateMessageType.mutexGranted: mutexes.markGranted(payload as int); } @@ -262,8 +275,13 @@ Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { // This only takes effect in this isolate. isolateLogger.level = Level.ALL; isolateLogger.onRecord.listen((record) { - var copy = LogRecord(record.level, record.message, record.loggerName, - record.error, record.stackTrace); + var copy = LogRecord( + record.level, + record.message, + record.loggerName, + record.error, + record.stackTrace, + ); sPort.sendLog(copy); }); @@ -273,8 +291,9 @@ Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { return r.future; } - Future prefetchCredentials( - {required bool invalidate}) async { + Future prefetchCredentials({ + required bool invalidate, + }) async { final r = results.createPending(); sPort.sendPrefetchCredentials(r.completer, invalidate); return r.future; @@ -286,33 +305,37 @@ Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { return r.future; } - runZonedGuarded(() async { - final storage = BucketStorage(database); - final sync = openedStreamingSync = StreamingSyncImplementation( - adapter: storage, - schemaJson: args.schemaJson, - connector: InternalConnector( - getCredentialsCached: getCredentialsCached, - prefetchCredentials: prefetchCredentials, - uploadCrud: uploadCrud, - ), - crudUpdateTriggerStream: database - .onChange(['ps_crud'], throttle: args.options.crudThrottleTime), - options: args.options, - syncMutex: mutexes.mutex('sync'), - crudMutex: mutexes.mutex('crud'), - ); - - sync.streamingSync(); - sync.statusStream.listen((event) { - sPort.sendStatus(event); - }); - }, (error, stack) async { - // Properly dispose the database if an uncaught error occurs. - // Unfortunately, this does not handle disposing while the database is opening. - // This should be rare - any uncaught error is a bug. And in most cases, - // it should occur after the database is already open. - await shutdown(); - Error.throwWithStackTrace(error, stack); - }); + runZonedGuarded( + () async { + final storage = BucketStorage(database); + final sync = openedStreamingSync = StreamingSyncImplementation( + adapter: storage, + schemaJson: args.schemaJson, + connector: InternalConnector( + getCredentialsCached: getCredentialsCached, + prefetchCredentials: prefetchCredentials, + uploadCrud: uploadCrud, + ), + crudUpdateTriggerStream: database.onChange([ + 'ps_crud', + ], throttle: args.options.crudThrottleTime), + options: args.options, + syncMutex: mutexes.mutex('sync'), + crudMutex: mutexes.mutex('crud'), + ); + + sync.streamingSync(); + sync.statusStream.listen((event) { + sPort.sendStatus(event); + }); + }, + (error, stack) async { + // Properly dispose the database if an uncaught error occurs. + // Unfortunately, this does not handle disposing while the database is opening. + // This should be rare - any uncaught error is a bug. And in most cases, + // it should occur after the database is already open. + await shutdown(); + Error.throwWithStackTrace(error, stack); + }, + ); } diff --git a/packages/powersync/lib/src/database/native/sync_isolate_protocol.dart b/packages/powersync/lib/src/database/native/sync_isolate_protocol.dart index a8dc9551..1ee824cb 100644 --- a/packages/powersync/lib/src/database/native/sync_isolate_protocol.dart +++ b/packages/powersync/lib/src/database/native/sync_isolate_protocol.dart @@ -41,7 +41,7 @@ enum SyncIsolateToClientMessageType { mutexAcquire, /// The sync isolate wants to release a mutex, payload is an [int] request id. - mutexRelease; + mutexRelease, } enum ClientToSyncIsolateMessageType { @@ -89,14 +89,19 @@ extension type SyncClientPort(SendPort port) { } void sendGetCredentialsCached( - PortCompleter completer) { + PortCompleter completer, + ) { send(SyncIsolateToClientMessageType.getCredentialsCached, completer); } void sendPrefetchCredentials( - PortCompleter completer, bool invalidate) { - send(SyncIsolateToClientMessageType.prefetchCredentials, - (completer, invalidate)); + PortCompleter completer, + bool invalidate, + ) { + send(SyncIsolateToClientMessageType.prefetchCredentials, ( + completer, + invalidate, + )); } void sendUploadCrud(PortCompleter completer) { @@ -200,8 +205,10 @@ final class _RemoteMutex implements Mutex { _RemoteMutex(this._server, this.name); @override - Future lock(Future Function() callback, - {Future? abortTrigger}) async { + Future lock( + Future Function() callback, { + Future? abortTrigger, + }) async { final grant = await _server.acquire(name); try { return await callback(); diff --git a/packages/powersync/lib/src/database/powersync_database.dart b/packages/powersync/lib/src/database/powersync_database.dart index ea034dd0..bea6aa4f 100644 --- a/packages/powersync/lib/src/database/powersync_database.dart +++ b/packages/powersync/lib/src/database/powersync_database.dart @@ -32,7 +32,9 @@ import 'encryption_options.dart'; const powerSyncDefaultSqliteOptions = SqliteOptions( webSqliteOptions: WebSqliteOptions( - wasmUri: 'sqlite3.wasm', workerUri: 'powersync_db.worker.js'), + wasmUri: 'sqlite3.wasm', + workerUri: 'powersync_db.worker.js', + ), ); /// A PowerSync managed database. @@ -205,14 +207,15 @@ abstract base class PowerSyncDatabase extends SqliteConnection { // Get version String version; try { - final row = - await database.get('SELECT powersync_rs_version() as version'); + final row = await database.get( + 'SELECT powersync_rs_version() as version', + ); version = row['version'] as String; } catch (e) { throw SqliteException( - extendedResultCode: 1, - message: - 'The powersync extension is not loaded correctly. Details: $e'); + extendedResultCode: 1, + message: 'The powersync extension is not loaded correctly. Details: $e', + ); } PowerSyncCoreVersion.parse(version).checkSupported(); @@ -383,8 +386,9 @@ abstract base class PowerSyncDatabase extends SqliteConnection { _connections.checkNotConnected(); this.schema = schema; - await database - .writeTransaction((tx) => schema_logic.updateSchema(tx, schema)); + await database.writeTransaction( + (tx) => schema_logic.updateSchema(tx, schema), + ); }); } @@ -396,13 +400,17 @@ abstract base class PowerSyncDatabase extends SqliteConnection { } /// Get upload queue size estimate and count. - Future getUploadQueueStats( - {bool includeSize = false}) async { + Future getUploadQueueStats({ + bool includeSize = false, + }) async { if (includeSize) { final row = await getOptional( - 'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud'); + 'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud', + ); return UploadQueueStats( - count: row?['count'] as int? ?? 0, size: row?['size'] as int? ?? 0); + count: row?['count'] as int? ?? 0, + size: row?['size'] as int? ?? 0, + ); } else { final row = await getOptional('SELECT count(*) as count FROM ps_crud'); return UploadQueueStats(count: row?['count'] as int? ?? 0); @@ -426,8 +434,9 @@ abstract base class PowerSyncDatabase extends SqliteConnection { /// and a single transaction may be split over multiple batches. Future getCrudBatch({int limit = 100}) async { final rows = await getAll( - 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', - [limit + 1]); + 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', + [limit + 1], + ); List all = [for (var row in rows) CrudEntry.fromRow(row)]; var haveMore = false; @@ -532,37 +541,48 @@ SELECT * FROM crud_entries; @override Future readTransaction( - Future Function(SqliteReadContext tx) callback, - {Duration? lockTimeout}) async { + Future Function(SqliteReadContext tx) callback, { + Duration? lockTimeout, + }) async { await isInitialized; return database.readTransaction(callback, lockTimeout: lockTimeout); } @override Future abortableReadLock( - Future Function(SqliteReadContext tx) callback, - {Future? abortTrigger, - String? debugContext}) async { + Future Function(SqliteReadContext tx) callback, { + Future? abortTrigger, + String? debugContext, + }) async { await isInitialized; - return database.abortableReadLock(callback, - abortTrigger: abortTrigger, debugContext: debugContext); + return database.abortableReadLock( + callback, + abortTrigger: abortTrigger, + debugContext: debugContext, + ); } @override Future abortableWriteLock( - Future Function(SqliteWriteContext tx) callback, - {Future? abortTrigger, - String? debugContext}) async { + Future Function(SqliteWriteContext tx) callback, { + Future? abortTrigger, + String? debugContext, + }) async { await isInitialized; - return database.abortableWriteLock(callback, - abortTrigger: abortTrigger, debugContext: debugContext); + return database.abortableWriteLock( + callback, + abortTrigger: abortTrigger, + debugContext: debugContext, + ); } @override - Stream watch(String sql, - {List parameters = const [], - Duration? throttle = const Duration(milliseconds: 30), - Iterable? triggerOnTables}) { + Stream watch( + String sql, { + List parameters = const [], + Duration? throttle = const Duration(milliseconds: 30), + Iterable? triggerOnTables, + }) { if (triggerOnTables == null || triggerOnTables.isEmpty) { return database.watch(sql, parameters: parameters, throttle: throttle); } @@ -572,10 +592,12 @@ SELECT * FROM crud_entries; powersyncTables.add(_prefixTableNames(tableName, 'ps_data__')); powersyncTables.add(_prefixTableNames(tableName, 'ps_data_local__')); } - return database.watch(sql, - parameters: parameters, - throttle: throttle, - triggerOnTables: powersyncTables); + return database.watch( + sql, + parameters: parameters, + throttle: throttle, + triggerOnTables: powersyncTables, + ); } @protected @@ -617,9 +639,11 @@ abstract base class BasePowerSyncDatabase extends PowerSyncDatabase { @override final Logger logger; - BasePowerSyncDatabase( - {required this.schema, required this.database, required this.logger}) - : super._() { + BasePowerSyncDatabase({ + required this.schema, + required this.database, + required this.logger, + }) : super._() { isInitialized = baseInit(); } @@ -644,8 +668,9 @@ abstract base class BasePowerSyncDatabase extends PowerSyncDatabase { await database.initialize(); await _checkVersion(); - await database - .writeTransaction((tx) => tx.execute('SELECT powersync_init();')); + await database.writeTransaction( + (tx) => tx.execute('SELECT powersync_init();'), + ); await updateSchema(schema); await _connections.resolveOfflineSyncStatus(); } @@ -653,10 +678,12 @@ abstract base class BasePowerSyncDatabase extends PowerSyncDatabase { @internal Stream powerSyncUpdateNotifications( - Stream inner) { + Stream inner, +) { return inner - .map((update) => - PowerSyncUpdateNotification.fromUpdateNotification(update)) + .map( + (update) => PowerSyncUpdateNotification.fromUpdateNotification(update), + ) .where((update) => update.isNotEmpty) .cast(); } diff --git a/packages/powersync/lib/src/database/web/web_powersync_database.dart b/packages/powersync/lib/src/database/web/web_powersync_database.dart index 2ac85502..975b6388 100644 --- a/packages/powersync/lib/src/database/web/web_powersync_database.dart +++ b/packages/powersync/lib/src/database/web/web_powersync_database.dart @@ -24,8 +24,11 @@ import '../../web/sync_controller.dart'; /// All changes to local tables are automatically recorded, whether connected /// or not. Once connected, the changes are uploaded. final class WebPowerSyncDatabase extends BasePowerSyncDatabase { - WebPowerSyncDatabase( - {required super.schema, required super.database, required super.logger}); + WebPowerSyncDatabase({ + required super.schema, + required super.database, + required super.logger, + }); @override @internal @@ -43,7 +46,8 @@ final class WebPowerSyncDatabase extends BasePowerSyncDatabase { // duplicating work across tabs. try { final workerUri = Uri.parse( - database.openFactory.sqliteOptions.webSqliteOptions.workerUri); + database.openFactory.sqliteOptions.webSqliteOptions.workerUri, + ); // This only affects our tests, where webSqliteOptions.workerUri is a blob // loading the worker. Using this as a sync worker seems to cause the test // runner to hang, so we want to throw an assertion error and continue @@ -65,8 +69,9 @@ final class WebPowerSyncDatabase extends BasePowerSyncDatabase { 'Could not use shared worker for synchronization, falling back to locks.', e, ); - final crudStream = - database.onChange(['ps_crud'], throttle: options.crudThrottleTime); + final crudStream = database.onChange([ + 'ps_crud', + ], throttle: options.crudThrottleTime); sync = StreamingSyncImplementation( adapter: storage, @@ -99,18 +104,25 @@ final class WebPowerSyncDatabase extends BasePowerSyncDatabase { /// /// In most cases, [readTransaction] should be used instead. @override - Future readLock(Future Function(SqliteReadContext tx) callback, - {String? debugContext, Duration? lockTimeout}) async { + Future readLock( + Future Function(SqliteReadContext tx) callback, { + String? debugContext, + Duration? lockTimeout, + }) async { await isInitialized; - return database.readLock(callback, - debugContext: debugContext, lockTimeout: lockTimeout); + return database.readLock( + callback, + debugContext: debugContext, + lockTimeout: lockTimeout, + ); } @override Future readTransaction( - Future Function(SqliteReadContext tx) callback, - {Duration? lockTimeout, - String? debugContext}) async { + Future Function(SqliteReadContext tx) callback, { + Duration? lockTimeout, + String? debugContext, + }) async { await isInitialized; return database.readTransaction(callback, lockTimeout: lockTimeout); } @@ -119,11 +131,17 @@ final class WebPowerSyncDatabase extends BasePowerSyncDatabase { /// /// In most cases, [writeTransaction] should be used instead. @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {String? debugContext, Duration? lockTimeout}) async { + Future writeLock( + Future Function(SqliteWriteContext tx) callback, { + String? debugContext, + Duration? lockTimeout, + }) async { await isInitialized; - return database.writeLock(callback, - debugContext: debugContext, lockTimeout: lockTimeout); + return database.writeLock( + callback, + debugContext: debugContext, + lockTimeout: lockTimeout, + ); } /// Uses the database writeTransaction instead of the locally @@ -131,9 +149,10 @@ final class WebPowerSyncDatabase extends BasePowerSyncDatabase { /// tracking to be correctly configured. @override Future writeTransaction( - Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, - String? debugContext}) async { + Future Function(SqliteWriteContext tx) callback, { + Duration? lockTimeout, + String? debugContext, + }) async { await isInitialized; return database.writeTransaction(callback, lockTimeout: lockTimeout); } diff --git a/packages/powersync/lib/src/devtools/extension.dart b/packages/powersync/lib/src/devtools/extension.dart index 8386082a..e0ac0137 100644 --- a/packages/powersync/lib/src/devtools/extension.dart +++ b/packages/powersync/lib/src/devtools/extension.dart @@ -35,8 +35,9 @@ final class PowerSyncDevToolsExtension { .map(decodeSqlValue) .toList(); - final rs = await tracked.database - .writeLock((ctx) => ctx.getAll(sql, sqlParameters)); + final rs = await tracked.database.writeLock( + (ctx) => ctx.getAll(sql, sqlParameters), + ); return { 'columnNames': rs.columnNames, 'rows': [ @@ -47,8 +48,10 @@ final class PowerSyncDevToolsExtension { case 'schema': return tracked.database.schema.toJson(); case 'table-updates-listen': - final stream = tracked.database - .onChange(null, throttle: const Duration(milliseconds: 100)); + final stream = tracked.database.onChange( + null, + throttle: const Duration(milliseconds: 100), + ); final id = _subscriptionId++; _clientSubscriptions[id] = stream.listen((updateNotification) { postEvent('table-updates', { @@ -106,27 +109,30 @@ final class PowerSyncDevToolsExtension { registerExtension('ext.powersync.version', (method, parameters) async { return ServiceExtensionResponse.result( - json.encode({'version': libraryVersion})); + json.encode({'version': libraryVersion}), + ); }); registerExtension('ext.powersync.list', (method, parameters) async { - return ServiceExtensionResponse.result(json.encode({ - 'databases': [ - for (final db in ExposedPowerSyncDatabase.byId.values) - { - 'id': db.id, - 'path': db.database.group.identifier, - 'name': p.basename(db.database.group.identifier), - 'lastCredentials': switch (db.lastCredentials) { - null => null, - final credentials => { + return ServiceExtensionResponse.result( + json.encode({ + 'databases': [ + for (final db in ExposedPowerSyncDatabase.byId.values) + { + 'id': db.id, + 'path': db.database.group.identifier, + 'name': p.basename(db.database.group.identifier), + 'lastCredentials': switch (db.lastCredentials) { + null => null, + final credentials => { 'endpoint': credentials.endpoint, 'token': credentials.token, }, - } - } - ] - })); + }, + }, + ], + }), + ); }); } } diff --git a/packages/powersync/lib/src/devtools/protocol.dart b/packages/powersync/lib/src/devtools/protocol.dart index c6ba8b4d..be27866a 100644 --- a/packages/powersync/lib/src/devtools/protocol.dart +++ b/packages/powersync/lib/src/devtools/protocol.dart @@ -29,8 +29,8 @@ Object? serializeSyncStatus(SyncStatus status) { 'downloadProgress': switch (status.downloadProgress) { null => null, final progress => DownloadProgress( - InternalSyncDownloadProgress.ofPublic(progress).buckets) - .toJson() + InternalSyncDownloadProgress.ofPublic(progress).buckets, + ).toJson(), }, 'uploading': status.uploading, 'lastSyncedAt': status.lastSyncedAt?.millisecondsSinceEpoch, @@ -42,10 +42,11 @@ Object? serializeSyncStatus(SyncStatus status) { 'priority': entry.priority.priorityNumber, 'lastSyncedAt': entry.lastSyncedAt?.millisecondsSinceEpoch, 'hasSynced': entry.hasSynced, - } + }, ], - 'internalSubscriptions': - status.internalSubscriptions?.map((s) => s.toJson()).toList(), + 'internalSubscriptions': status.internalSubscriptions + ?.map((s) => s.toJson()) + .toList(), }; } @@ -63,29 +64,34 @@ SyncStatus deserializeSyncStatus(Map serialized) { downloadProgress: switch (serialized['downloadProgress']) { null => null, final downloadProgress => InternalSyncDownloadProgress( - DownloadProgress.fromJson( - downloadProgress as Map) - .buckets) - .asSyncDownloadProgress + DownloadProgress.fromJson( + downloadProgress as Map, + ).buckets, + ).asSyncDownloadProgress, }, uploading: serialized['uploading'] as bool, lastSyncedAt: readDateTime(serialized['lastSyncedAt'] as int?), uploadError: serialized['uploadError'], downloadError: serialized['downloadError'], priorityStatusEntries: [ - for (final entry in (serialized['priorityStatusEntries'] as List) - .cast>()) + for (final entry + in (serialized['priorityStatusEntries'] as List) + .cast>()) ( priority: StreamPriority(entry['priority'] as int), lastSyncedAt: readDateTime(entry['lastSyncedAt']), - hasSynced: entry['hasSynced'] as bool? - ) + hasSynced: entry['hasSynced'] as bool?, + ), ], streamSubscriptions: switch (serialized['internalSubscriptions']) { - final List entries => entries - .map((e) => - CoreActiveStreamSubscription.fromJson(e as Map)) - .toList(), + final List entries => + entries + .map( + (e) => CoreActiveStreamSubscription.fromJson( + e as Map, + ), + ) + .toList(), _ => null, }, ); diff --git a/packages/powersync/lib/src/exceptions.dart b/packages/powersync/lib/src/exceptions.dart index bc35df4a..e5329103 100644 --- a/packages/powersync/lib/src/exceptions.dart +++ b/packages/powersync/lib/src/exceptions.dart @@ -35,7 +35,8 @@ class PowerSyncProtocolException implements Exception { class SyncResponseException implements Exception { /// Parse an error response from the PowerSync service static Future fromStreamedResponse( - http.StreamedResponse response) async { + http.StreamedResponse response, + ) async { try { final body = await response.stream.bytesToString(); return _fromResponseBody(response, body); @@ -59,9 +60,12 @@ class SyncResponseException implements Exception { } static SyncResponseException _fromResponseBody( - http.BaseResponse response, String body) { + http.BaseResponse response, + String body, + ) { final decoded = convert.jsonDecode(body); - final details = switch (decoded['error']) { + final details = + switch (decoded['error']) { final Map details => _errorDescription(details), _ => null, } ?? diff --git a/packages/powersync/lib/src/isolate_completer.dart b/packages/powersync/lib/src/isolate_completer.dart index c82fa9af..7ac55303 100644 --- a/packages/powersync/lib/src/isolate_completer.dart +++ b/packages/powersync/lib/src/isolate_completer.dart @@ -121,8 +121,10 @@ class PortCompleter { } } - Future handle(FutureOr Function() callback, - {bool ignoreStackTrace = false}) async { + Future handle( + FutureOr Function() callback, { + bool ignoreStackTrace = false, + }) async { Isolate.current.addOnExitListener(sendPort, response: abortedResponse); try { @@ -147,14 +149,14 @@ class PortResult { final StackTrace? stackTrace; const PortResult.success(T result) - : success = true, - _error = null, - stackTrace = null, - _result = result; + : success = true, + _error = null, + stackTrace = null, + _result = result; const PortResult.error(Object error, [this.stackTrace]) - : success = false, - _result = null, - _error = error; + : success = false, + _result = null, + _error = error; void _applyTo(Completer completer) { if (success) { diff --git a/packages/powersync/lib/src/log.dart b/packages/powersync/lib/src/log.dart index c628295b..fe643f5a 100644 --- a/packages/powersync/lib/src/log.dart +++ b/packages/powersync/lib/src/log.dart @@ -18,7 +18,8 @@ Logger _makeDebugLogger() { logger.level = Level.FINE; logger.onRecord.listen((record) { print( - '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}', + ); if (record.error != null) { print(record.error); diff --git a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart index 459e6ca3..666838ce 100644 --- a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart @@ -16,8 +16,11 @@ var _didInstallExtension = false; base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { final EncryptionOptions? encryptionOptions; - NativePowerSyncOpenFactory( - {required super.path, super.sqliteOptions, this.encryptionOptions}); + NativePowerSyncOpenFactory({ + required super.path, + super.sqliteOptions, + this.encryptionOptions, + }); @override List pragmaStatements(SqliteOpenOptions options) { @@ -30,7 +33,8 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { void enableExtension() { if (!_didInstallExtension) { final entrypoint = Native.addressOf>( - sqlite3_powersync_init); + sqlite3_powersync_init, + ); sqlite3.ensureExtensionLoaded(SqliteExtension(entrypoint.cast())); diff --git a/packages/powersync/lib/src/open_factory/native/sqlite3_powersync_init.dart b/packages/powersync/lib/src/open_factory/native/sqlite3_powersync_init.dart index d3db1587..0fd4e594 100644 --- a/packages/powersync/lib/src/open_factory/native/sqlite3_powersync_init.dart +++ b/packages/powersync/lib/src/open_factory/native/sqlite3_powersync_init.dart @@ -1,7 +1,7 @@ import 'dart:ffi'; -typedef ExtensionEntrypoint = Int Function( - Pointer, Pointer, Pointer); +typedef ExtensionEntrypoint = + Int Function(Pointer, Pointer, Pointer); @Native() // ignore: non_constant_identifier_names diff --git a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart b/packages/powersync/lib/src/open_factory/web/web_open_factory.dart index 81ed2f99..8f9a18c5 100644 --- a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/web/web_open_factory.dart @@ -14,15 +14,19 @@ import '../../web/worker_utils.dart'; base class WebPowerSyncOpenFactory extends WebSqliteOpenFactory { final EncryptionOptions? encryptionOptions; - WebPowerSyncOpenFactory( - {required super.path, super.sqliteOptions, this.encryptionOptions}); + WebPowerSyncOpenFactory({ + required super.path, + super.sqliteOptions, + this.encryptionOptions, + }); @override Future openWebSqlite(WebSqliteOptions options) async { return WebSqlite.open( wasmModule: sqliteOptions.webSqliteOptions.wasmUri, - workers: - PowerSyncWorkerConnector(sqliteOptions.webSqliteOptions.workerUri), + workers: PowerSyncWorkerConnector( + sqliteOptions.webSqliteOptions.workerUri, + ), controller: PowerSyncAsyncSqliteController(), handleCustomRequest: handleCustomRequest, ); @@ -30,11 +34,16 @@ base class WebPowerSyncOpenFactory extends WebSqliteOpenFactory { @override Future connectToWorker( - WebSqlite sqlite, String name) { - return sqlite.connectToRecommended(name, - additionalOptions: PowerSyncAdditionalOpenOptions( - useMultipleCiphersVfs: encryptionOptions != null), - preparedStatementCacheSize: sqliteOptions.preparedStatementCacheSize); + WebSqlite sqlite, + String name, + ) { + return sqlite.connectToRecommended( + name, + additionalOptions: PowerSyncAdditionalOpenOptions( + useMultipleCiphersVfs: encryptionOptions != null, + ), + preparedStatementCacheSize: sqliteOptions.preparedStatementCacheSize, + ); } @override diff --git a/packages/powersync/lib/src/platform_specific/native.dart b/packages/powersync/lib/src/platform_specific/native.dart index 0a3478bc..b18fc41e 100644 --- a/packages/powersync/lib/src/platform_specific/native.dart +++ b/packages/powersync/lib/src/platform_specific/native.dart @@ -12,9 +12,15 @@ Mutex potentiallySharedMutex(String identifier) { } SqliteOpenFactory powerSyncOpenFactory( - String path, SqliteOptions options, EncryptionOptions? encryption) { + String path, + SqliteOptions options, + EncryptionOptions? encryption, +) { return NativePowerSyncOpenFactory( - path: path, sqliteOptions: options, encryptionOptions: encryption); + path: path, + sqliteOptions: options, + encryptionOptions: encryption, + ); } BasePowerSyncDatabase openPowerSyncDatabase( diff --git a/packages/powersync/lib/src/platform_specific/unsupported.dart b/packages/powersync/lib/src/platform_specific/unsupported.dart index 81e4006f..e51f307e 100644 --- a/packages/powersync/lib/src/platform_specific/unsupported.dart +++ b/packages/powersync/lib/src/platform_specific/unsupported.dart @@ -18,7 +18,10 @@ Mutex potentiallySharedMutex(String identifier) { } SqliteOpenFactory powerSyncOpenFactory( - String path, SqliteOptions options, EncryptionOptions? encryption) { + String path, + SqliteOptions options, + EncryptionOptions? encryption, +) { _unsupportedPlatform(); } diff --git a/packages/powersync/lib/src/platform_specific/web.dart b/packages/powersync/lib/src/platform_specific/web.dart index 4d6ca09b..59d54152 100644 --- a/packages/powersync/lib/src/platform_specific/web.dart +++ b/packages/powersync/lib/src/platform_specific/web.dart @@ -18,9 +18,15 @@ Mutex potentiallySharedMutex(String identifier) { } SqliteOpenFactory powerSyncOpenFactory( - String path, SqliteOptions options, EncryptionOptions? encryption) { + String path, + SqliteOptions options, + EncryptionOptions? encryption, +) { return WebPowerSyncOpenFactory( - path: path, sqliteOptions: options, encryptionOptions: encryption); + path: path, + sqliteOptions: options, + encryptionOptions: encryption, + ); } BasePowerSyncDatabase openPowerSyncDatabase( diff --git a/packages/powersync/lib/src/powersync_update_notification.dart b/packages/powersync/lib/src/powersync_update_notification.dart index 44b03078..99da7cf3 100644 --- a/packages/powersync/lib/src/powersync_update_notification.dart +++ b/packages/powersync/lib/src/powersync_update_notification.dart @@ -5,12 +5,14 @@ class PowerSyncUpdateNotification extends UpdateNotification { PowerSyncUpdateNotification(super.tables); factory PowerSyncUpdateNotification.fromRawTables( - Iterable originalTables) { + Iterable originalTables, + ) { return PowerSyncUpdateNotification(_friendlyTableNames(originalTables)); } factory PowerSyncUpdateNotification.fromUpdateNotification( - UpdateNotification updateNotification) { + UpdateNotification updateNotification, + ) { return PowerSyncUpdateNotification.fromRawTables(updateNotification.tables); } @@ -31,8 +33,11 @@ class PowerSyncUpdateNotification extends UpdateNotification { if (other is PowerSyncUpdateNotification) { return PowerSyncUpdateNotification(tables.union(other.tables)); } else { - return PowerSyncUpdateNotification(tables.union( - PowerSyncUpdateNotification.fromUpdateNotification(other).tables)); + return PowerSyncUpdateNotification( + tables.union( + PowerSyncUpdateNotification.fromUpdateNotification(other).tables, + ), + ); } } diff --git a/packages/powersync/lib/src/schema.dart b/packages/powersync/lib/src/schema.dart index 8d854b81..a3333bc0 100644 --- a/packages/powersync/lib/src/schema.dart +++ b/packages/powersync/lib/src/schema.dart @@ -58,8 +58,10 @@ final class TrackPreviousValuesOptions { /// instead of always including all old values. final bool onlyWhenChanged; - const TrackPreviousValuesOptions( - {this.columnFilter, this.onlyWhenChanged = false}); + const TrackPreviousValuesOptions({ + this.columnFilter, + this.onlyWhenChanged = false, + }); } /// Common options that can be applied on [Table] and [RawTable] (through @@ -119,7 +121,8 @@ final class TableOptions { /// A single table in the schema. @Deprecated.subclass( - 'Avoid extending table, create an instance or extension type around it instead.') + 'Avoid extending table, create an instance or extension type around it instead.', +) base class Table extends TableOptions { static const _maxNumberOfColumns = 1999; @@ -164,16 +167,19 @@ base class Table extends TableOptions { super.ignoreEmptyUpdates, super.trackMetadata, super.trackPreviousValues, - }) : _viewNameOverride = viewName, - super(insertOnly: false); + }) : _viewNameOverride = viewName, + super(insertOnly: false); /// Create a table that only exists locally. /// /// This table does not record changes, and is not synchronized from the service. - const Table.localOnly(this.name, this.columns, - {this.indexes = const [], String? viewName}) - : _viewNameOverride = viewName, - super(localOnly: true); + const Table.localOnly( + this.name, + this.columns, { + this.indexes = const [], + String? viewName, + }) : _viewNameOverride = viewName, + super(localOnly: true); /// Create a table that only supports inserts. /// @@ -190,9 +196,9 @@ base class Table extends TableOptions { super.ignoreEmptyUpdates, super.trackMetadata, super.trackPreviousValues, - }) : indexes = const [], - _viewNameOverride = viewName, - super(localOnly: false, insertOnly: true); + }) : indexes = const [], + _viewNameOverride = viewName, + super(localOnly: false, insertOnly: true); Column operator [](String columnName) { return columns.firstWhere((element) => element.name == columnName); @@ -208,7 +214,8 @@ base class Table extends TableOptions { void validate() { if (columns.length > _maxNumberOfColumns) { throw AssertionError( - "Table $name has more than $_maxNumberOfColumns columns, which is not supported"); + "Table $name has more than $_maxNumberOfColumns columns, which is not supported", + ); } if (invalidSqliteCharacters.hasMatch(name)) { @@ -218,7 +225,8 @@ base class Table extends TableOptions { if (_viewNameOverride != null && invalidSqliteCharacters.hasMatch(_viewNameOverride)) { throw AssertionError( - "Invalid characters in view name: $_viewNameOverride"); + "Invalid characters in view name: $_viewNameOverride", + ); } _validateOptions(); @@ -227,12 +235,14 @@ base class Table extends TableOptions { for (var column in columns) { if (column.name == 'id') { throw AssertionError( - "$name: id column is automatically added, custom id columns are not supported"); + "$name: id column is automatically added, custom id columns are not supported", + ); } else if (columnNames.contains(column.name)) { throw AssertionError("Duplicate column $name.${column.name}"); } else if (invalidSqliteCharacters.hasMatch(column.name)) { throw AssertionError( - "Invalid characters in column name: $name.${column.name}"); + "Invalid characters in column name: $name.${column.name}", + ); } columnNames.add(column.name); @@ -244,13 +254,15 @@ base class Table extends TableOptions { throw AssertionError("Duplicate index $name.${index.name}"); } else if (invalidSqliteCharacters.hasMatch(index.name)) { throw AssertionError( - "Invalid characters in index name: $name.${index.name}"); + "Invalid characters in index name: $name.${index.name}", + ); } for (var column in index.columns) { if (!columnNames.contains(column.column)) { throw AssertionError( - "Column $name.${column.column} not found for index ${index.name}"); + "Column $name.${column.column} not found for index ${index.name}", + ); } } @@ -265,12 +277,12 @@ base class Table extends TableOptions { } Map toJson() => { - 'name': name, - 'view_name': _viewNameOverride, - 'columns': columns, - 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false), - ..._optionsToJson(), - }; + 'name': name, + 'view_name': _viewNameOverride, + 'columns': columns, + 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false), + ..._optionsToJson(), + }; } class Index { @@ -285,8 +297,10 @@ class Index { /// Construct a new index with the specified column names. factory Index.ascending(String name, List columns) { - return Index(name, - columns.map((e) => IndexedColumn.ascending(e)).toList(growable: false)); + return Index( + name, + columns.map((e) => IndexedColumn.ascending(e)).toList(growable: false), + ); } /// Internal use only. @@ -297,9 +311,9 @@ class Index { } Map toJson(Table table) => { - 'name': name, - 'columns': columns.map((c) => c.toJson(table)).toList(growable: false) - }; + 'name': name, + 'columns': columns.map((c) => c.toJson(table)).toList(growable: false), + }; } /// Describes an indexed column. @@ -420,12 +434,12 @@ final class RawTable { }); Map toJson() => { - 'name': name, - 'put': put, - 'delete': delete, - 'clear': clear, - ...?schema?._toJson(this), - }; + 'name': name, + 'put': put, + 'delete': delete, + 'clear': clear, + ...?schema?._toJson(this), + }; } /// The schema of a [RawTable] in the local database. @@ -464,10 +478,10 @@ final class RawTableSchema { }); Map _toJson(RawTable tbl) => { - 'table_name': tableName ?? tbl.name, - if (syncedColumns != null) 'synced_columns': syncedColumns, - ...options._optionsToJson(), - }; + 'table_name': tableName ?? tbl.name, + if (syncedColumns != null) 'synced_columns': syncedColumns, + ...options._optionsToJson(), + }; } /// An SQL statement to be run by the sync client against raw tables. @@ -490,10 +504,7 @@ final class PendingStatement { const PendingStatement({required this.sql, required this.params}); - Map toJson() => { - 'sql': sql, - 'params': params, - }; + Map toJson() => {'sql': sql, 'params': params}; } /// A description of a value that will be resolved in the sync client when @@ -520,9 +531,7 @@ class _PendingStmtValueColumn implements PendingStatementValue { @override dynamic toJson() { - return { - 'Column': column, - }; + return {'Column': column}; } } diff --git a/packages/powersync/lib/src/setup/web.dart b/packages/powersync/lib/src/setup/web.dart index faf064a7..44789168 100644 --- a/packages/powersync/lib/src/setup/web.dart +++ b/packages/powersync/lib/src/setup/web.dart @@ -62,9 +62,14 @@ Future downloadWebAssets(List arguments) async { try { const sqlitePackageName = 'sqlite3'; - final (tag: powersyncTag, version: powerSyncVersion) = - await powerSyncVersionOrLatest( - httpClient, packageConfig, packageConfigFile); + final ( + tag: powersyncTag, + version: powerSyncVersion, + ) = await powerSyncVersionOrLatest( + httpClient, + packageConfig, + packageConfigFile, + ); final firstPowerSyncVersionWithOwnWasm = Version(1, 12, 0); if (downloadWorker) { @@ -76,8 +81,10 @@ Future downloadWebAssets(List arguments) async { final oldSyncWorker = File(syncWorkerPath); if (await oldSyncWorker.exists()) { await oldSyncWorker.delete(); - print('Deleted powersync_sync.worker.js. Going forward, ' - 'powersync_db.worker.js is the only worker required.'); + print( + 'Deleted powersync_sync.worker.js. Going forward, ' + 'powersync_db.worker.js is the only worker required.', + ); } } @@ -93,15 +100,18 @@ Future downloadWebAssets(List arguments) async { "v${getPubspecVersion(packageConfigFile, sqlite3Pkg, sqlitePackageName)}"; List tags = await getLatestTagsFromRelease(httpClient); - String? matchTag = tags.firstWhereOrNull((element) => - element.contains(sqlite3Version) && coreVersionIsInRange(element)); + String? matchTag = tags.firstWhereOrNull( + (element) => + element.contains(sqlite3Version) && coreVersionIsInRange(element), + ); if (matchTag != null) { sqlite3Version = matchTag; } else { throw Exception( - """No compatible powersync core version found for sqlite3 version $sqlite3Version + """No compatible powersync core version found for sqlite3 version $sqlite3Version Latest supported sqlite3 versions: ${tags.take(3).map((tag) => tag.split('-')[0]).join(', ')}. - You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases"""); + You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases""", + ); } final sqliteUrl = @@ -118,25 +128,36 @@ Future downloadWebAssets(List arguments) async { } Future<({String tag, Version version})> powerSyncVersionOrLatest( - HttpClient client, dynamic packageConfig, File packageConfigFile) async { + HttpClient client, + dynamic packageConfig, + File packageConfigFile, +) async { const powersyncPackageName = 'powersync'; // Don't require powersync dependency. The user has one if running this script // and we also want to support powersync_sqlcipher (for which we download // the latest versions). - final powersyncPkg = getPackageFromConfig(packageConfig, powersyncPackageName, - required: false); + final powersyncPkg = getPackageFromConfig( + packageConfig, + powersyncPackageName, + required: false, + ); if (powersyncPkg == null) { - final [tag, ...] = - await getLatestTagsFromRelease(client, repo: 'powersync.dart'); + final [tag, ...] = await getLatestTagsFromRelease( + client, + repo: 'powersync.dart', + ); return ( tag: tag, - version: Version.parse(tag.substring('powersync-v'.length)) + version: Version.parse(tag.substring('powersync-v'.length)), ); } - final powersyncVersion = - getPubspecVersion(packageConfigFile, powersyncPkg, powersyncPackageName); + final powersyncVersion = getPubspecVersion( + packageConfigFile, + powersyncPkg, + powersyncPackageName, + ); return ( tag: 'powersync-v$powersyncVersion', version: Version.parse(powersyncVersion), @@ -152,8 +173,9 @@ bool coreVersionIsInRange(String tag) { String powersyncPart = parts[1]; List versionParts = powersyncPart.split('.'); - String extractedVersion = - versionParts.sublist(versionParts.length - 3).join('.'); + String extractedVersion = versionParts + .sublist(versionParts.length - 3) + .join('.'); final coreVersion = Version.parse(extractedVersion); if (constraint.allows(coreVersion)) { return true; @@ -161,8 +183,11 @@ bool coreVersionIsInRange(String tag) { return false; } -dynamic getPackageFromConfig(dynamic packageConfig, String packageName, - {bool required = false}) { +dynamic getPackageFromConfig( + dynamic packageConfig, + String packageName, { + bool required = false, +}) { final pkg = (packageConfig['packages'] as List? ?? []).firstWhere( (dynamic e) => e['name'] == packageName, orElse: () => null, @@ -174,28 +199,37 @@ dynamic getPackageFromConfig(dynamic packageConfig, String packageName, } String getPubspecVersion( - File packageConfigFile, dynamic package, String packageName) { - final rootUri = - packageConfigFile.uri.resolve(package['rootUri'] as String? ?? ''); + File packageConfigFile, + dynamic package, + String packageName, +) { + final rootUri = packageConfigFile.uri.resolve( + package['rootUri'] as String? ?? '', + ); print('Using package:$packageName from ${rootUri.toFilePath()}'); - String pubspec = - File('${rootUri.toFilePath()}/pubspec.yaml').readAsStringSync(); + String pubspec = File( + '${rootUri.toFilePath()}/pubspec.yaml', + ).readAsStringSync(); Pubspec parsed = Pubspec.parse(pubspec); final version = parsed.version?.toString(); if (version == null) { throw Exception( - "${capitalize(packageName)} version not found. Run `flutter pub get` first."); + "${capitalize(packageName)} version not found. Run `flutter pub get` first.", + ); } return version; } String capitalize(String s) => s[0].toUpperCase() + s.substring(1); -Future> getLatestTagsFromRelease(HttpClient httpClient, - {String repo = 'sqlite3.dart'}) async { +Future> getLatestTagsFromRelease( + HttpClient httpClient, { + String repo = 'sqlite3.dart', +}) async { var request = await httpClient.getUrl( - Uri.parse("https://api.github.com/repos/powersync-ja/$repo/releases")); + Uri.parse("https://api.github.com/repos/powersync-ja/$repo/releases"), + ); var response = await request.close(); if (response.statusCode == HttpStatus.ok) { var res = await response.transform(utf8.decoder).join(); @@ -213,7 +247,10 @@ Future> getLatestTagsFromRelease(HttpClient httpClient, } Future downloadFile( - HttpClient httpClient, String url, String savePath) async { + HttpClient httpClient, + String url, + String savePath, +) async { print('Downloading: $url'); var request = await httpClient.getUrl(Uri.parse(url)); var response = await request.close(); @@ -222,7 +259,8 @@ Future downloadFile( await response.pipe(file.openWrite()); } else { throw Exception( - 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}', + ); } } @@ -234,7 +272,10 @@ Future downloadFile( /// Copying from there ensures we run web tests against our current SQLite web /// build and avoids downloading from GitHub releases for every package we test. Future _copyPrecompiled( - Directory project, String wasmFile, String outputDir) async { + Directory project, + String wasmFile, + String outputDir, +) async { // The package config will always be $root/.dart_tool/package_config.json final packageConfig = (await Isolate.packageConfig)!; @@ -242,7 +283,8 @@ Future _copyPrecompiled( // sqlite3_wasm_build package. final destination = p.join(project.path, outputDir); final wasmSource = File.fromUri( - packageConfig.resolve('../packages/sqlite3_wasm_build/dist/$wasmFile')); + packageConfig.resolve('../packages/sqlite3_wasm_build/dist/$wasmFile'), + ); print('Copying $wasmSource to $destination'); await wasmSource.copy(p.join(destination, wasmFile)); } diff --git a/packages/powersync/lib/src/sync/bucket_storage.dart b/packages/powersync/lib/src/sync/bucket_storage.dart index 20aa9665..62a2ad7e 100644 --- a/packages/powersync/lib/src/sync/bucket_storage.dart +++ b/packages/powersync/lib/src/sync/bucket_storage.dart @@ -13,8 +13,10 @@ import '../schema_logic.dart'; @internal extension type BucketStorage(SqliteConnection _internalDb) { // Use only for read statements - Future select(String query, - [List parameters = const []]) async { + Future select( + String query, [ + List parameters = const [], + ]) async { return await _internalDb.getAll(query, parameters); } @@ -24,9 +26,11 @@ extension type BucketStorage(SqliteConnection _internalDb) { } Future updateTargetCheckpointRequest( - Future Function() checkpointCallback) async { - final currentTarget = await _internalDb - .readTransaction((db) => db.targetCheckpointRequestId()); + Future Function() checkpointCallback, + ) async { + final currentTarget = await _internalDb.readTransaction( + (db) => db.targetCheckpointRequestId(), + ); if (currentTarget != maxOpId) { // Nothing to update @@ -34,7 +38,8 @@ extension type BucketStorage(SqliteConnection _internalDb) { } final rs = await select( - 'SELECT seq FROM main.sqlite_sequence WHERE name = \'ps_crud\''); + 'SELECT seq FROM main.sqlite_sequence WHERE name = \'ps_crud\'', + ); if (rs.isEmpty) { // Nothing to update return false; @@ -48,7 +53,8 @@ extension type BucketStorage(SqliteConnection _internalDb) { return false; } final rs = await tx.execute( - 'SELECT seq FROM main.sqlite_sequence WHERE name = \'ps_crud\''); + 'SELECT seq FROM main.sqlite_sequence WHERE name = \'ps_crud\'', + ); assert(rs.isNotEmpty); int seqAfter = rs.first['seq'] as int; @@ -63,8 +69,9 @@ extension type BucketStorage(SqliteConnection _internalDb) { } Future nextCrudItem() async { - var next = await _internalDb - .getOptional('SELECT * FROM ps_crud ORDER BY id ASC LIMIT 1'); + var next = await _internalDb.getOptional( + 'SELECT * FROM ps_crud ORDER BY id ASC LIMIT 1', + ); return next == null ? null : CrudEntry.fromRow(next); } @@ -79,8 +86,9 @@ extension type BucketStorage(SqliteConnection _internalDb) { return null; } - final rows = - await select('SELECT * FROM ps_crud ORDER BY id ASC LIMIT ?', [limit]); + final rows = await select('SELECT * FROM ps_crud ORDER BY id ASC LIMIT ?', [ + limit, + ]); List all = []; for (var row in rows) { all.add(CrudEntry.fromRow(row)); @@ -97,7 +105,8 @@ extension type BucketStorage(SqliteConnection _internalDb) { } Future Function({String? writeCheckpoint}) crudCompletionCallback( - int lastClientId) { + int lastClientId, + ) { return ({String? writeCheckpoint}) async { await _internalDb.writeTransaction((db) async { await db.execute('DELETE FROM ps_crud WHERE id <= ?', [lastClientId]); @@ -112,18 +121,18 @@ extension type BucketStorage(SqliteConnection _internalDb) { } Future control(String op, [Object? payload]) async { - return await _internalDb.writeTransaction( - (tx) async { - return (await tx._control(op, payload))!; - }, - ); + return await _internalDb.writeTransaction((tx) async { + return (await tx._control(op, payload))!; + }); } } extension PowerSyncControl on SqliteReadContext { Future _control(String op, [Object? payload]) async { - final row = await get( - 'SELECT CAST(powersync_control(?, ?) AS TEXT)', [op, payload]); + final row = await get('SELECT CAST(powersync_control(?, ?) AS TEXT)', [ + op, + payload, + ]); return row.columnAt(0) as String?; } diff --git a/packages/powersync/lib/src/sync/connection_manager.dart b/packages/powersync/lib/src/sync/connection_manager.dart index f3683c6c..c51fade0 100644 --- a/packages/powersync/lib/src/sync/connection_manager.dart +++ b/packages/powersync/lib/src/sync/connection_manager.dart @@ -95,9 +95,9 @@ final class ConnectionManager { } List get _subscribedStreams => [ - for (final active in _locallyActiveSubscriptions.values) - (name: active.name, parameters: active.encodedParameters) - ]; + for (final active in _locallyActiveSubscriptions.values) + (name: active.name, parameters: active.encodedParameters), + ]; Future connect({ required PowerSyncBackendConnector connector, @@ -106,7 +106,8 @@ final class ConnectionManager { if (db.schema.rawTables.isNotEmpty && options.source.syncImplementation != SyncClientImplementation.rust) { throw UnsupportedError( - 'Raw tables are only supported by the Rust client.'); + 'Raw tables are only supported by the Rust client.', + ); } var thisConnectAborter = AbortController(); @@ -185,15 +186,21 @@ final class ConnectionManager { } _SyncStreamSubscriptionHandle _referenceStreamSubscription( - String stream, Map? parameters) { + String stream, + Map? parameters, + ) { final key = (stream, json.encode(parameters)); _ActiveSubscription active; if (_locallyActiveSubscriptions[key] case final current?) { active = current; } else { - active = _ActiveSubscription(this, - name: stream, parameters: parameters, encodedParameters: key.$2); + active = _ActiveSubscription( + this, + name: stream, + parameters: parameters, + encodedParameters: key.$2, + ); _locallyActiveSubscriptions[key] = active; _subscriptionsChanged?.add(null); } @@ -203,17 +210,19 @@ final class ConnectionManager { void _clearSubscription(_ActiveSubscription subscription) { assert(subscription.refcount == 0); - _locallyActiveSubscriptions - .remove((subscription.name, subscription.encodedParameters)); + _locallyActiveSubscriptions.remove(( + subscription.name, + subscription.encodedParameters, + )); _subscriptionsChanged?.add(null); } Future _subscriptionsCommand(Object? command) async { await db.writeTransaction((tx) { - return tx.execute( - 'SELECT powersync_control(?, ?)', - ['subscriptions', json.encode(command)], - ); + return tx.execute('SELECT powersync_control(?, ?)', [ + 'subscriptions', + json.encode(command), + ]); }); _subscriptionsChanged?.add(null); } @@ -226,10 +235,7 @@ final class ConnectionManager { }) async { await _subscriptionsCommand({ 'subscribe': { - 'stream': { - 'name': stream, - 'params': parameters, - }, + 'stream': {'name': stream, 'params': parameters}, 'ttl': ttl?.inSeconds, 'priority': priority, }, @@ -250,10 +256,7 @@ final class ConnectionManager { required Object? parameters, }) async { await _subscriptionsCommand({ - 'unsubscribe': { - 'name': stream, - 'params': parameters, - }, + 'unsubscribe': {'name': stream, 'params': parameters}, }); } @@ -263,10 +266,12 @@ final class ConnectionManager { ); final status = CoreSyncStatus.fromJson( - json.decode(row['r'] as String) as Map); + json.decode(row['r'] as String) as Map, + ); manuallyChangeSyncStatus( - (MutableSyncStatus()..applyFromCore(status)).immutableSnapshot()); + (MutableSyncStatus()..applyFromCore(status)).immutableSnapshot(), + ); } SyncStream syncStream(String name, Map? parameters) { @@ -367,10 +372,11 @@ final class _SyncStreamSubscriptionHandle implements SyncStreamSubscription { static final Finalizer<_ActiveSubscription> _finalizer = Finalizer((sub) { sub.connections.db.logger.warning( - 'A subscription to ${sub.name} (with parameters ${sub.parameters}) ' - 'leaked! Please ensure calling SyncStreamSubscription.unsubscribe() ' - "when you don't need a subscription anymore. For global " - 'subscriptions, consider storing them in global fields to avoid this ' - 'warning.'); + 'A subscription to ${sub.name} (with parameters ${sub.parameters}) ' + 'leaked! Please ensure calling SyncStreamSubscription.unsubscribe() ' + "when you don't need a subscription anymore. For global " + 'subscriptions, consider storing them in global fields to avoid this ' + 'warning.', + ); }); } diff --git a/packages/powersync/lib/src/sync/instruction.dart b/packages/powersync/lib/src/sync/instruction.dart index c4d626d4..83b0bb1b 100644 --- a/packages/powersync/lib/src/sync/instruction.dart +++ b/packages/powersync/lib/src/sync/instruction.dart @@ -6,18 +6,22 @@ import 'sync_status.dart'; sealed class Instruction { factory Instruction.fromJson(Map json) { return switch (json) { - {'LogLine': final logLine} => - LogLine.fromJson(logLine as Map), - {'UpdateSyncStatus': final updateStatus} => - UpdateSyncStatus.fromJson(updateStatus as Map), - {'EstablishSyncStream': final establish} => - EstablishSyncStream.fromJson(establish as Map), - {'FetchCredentials': final creds} => - FetchCredentials.fromJson(creds as Map), + {'LogLine': final logLine} => LogLine.fromJson( + logLine as Map, + ), + {'UpdateSyncStatus': final updateStatus} => UpdateSyncStatus.fromJson( + updateStatus as Map, + ), + {'EstablishSyncStream': final establish} => EstablishSyncStream.fromJson( + establish as Map, + ), + {'FetchCredentials': final creds} => FetchCredentials.fromJson( + creds as Map, + ), {'CloseSyncStream': final closeOptions as Map} => CloseSyncStream(closeOptions['hide_disconnect'] as bool), {'DidCompleteSync': _} => const DidCompleteSync(), - _ => UnknownSyncInstruction(json) + _ => UnknownSyncInstruction(json), }; } } @@ -56,8 +60,8 @@ final class UpdateSyncStatus implements NonInterruptingInstruction { factory UpdateSyncStatus.fromJson(Map json) { return UpdateSyncStatus( - status: - CoreSyncStatus.fromJson(json['status'] as Map)); + status: CoreSyncStatus.fromJson(json['status'] as Map), + ); } } @@ -82,15 +86,18 @@ final class CoreSyncStatus { connecting: json['connecting'] as bool, priorityStatus: [ for (final entry in json['priority_status'] as List) - _priorityStatusFromJson(entry as Map) + _priorityStatusFromJson(entry as Map), ], downloading: switch (json['downloading']) { null => null, final raw as Map => DownloadProgress.fromJson(raw), }, streams: (json['streams'] as List) - .map((e) => - CoreActiveStreamSubscription.fromJson(e as Map)) + .map( + (e) => CoreActiveStreamSubscription.fromJson( + e as Map, + ), + ) .toList(), ); } @@ -101,8 +108,9 @@ final class CoreSyncStatus { hasSynced: json['has_synced'] as bool?, lastSyncedAt: switch (json['last_synced_at']) { null => null, - final lastSyncedAt as int => - DateTime.fromMicrosecondsSinceEpoch(lastSyncedAt), + final lastSyncedAt as int => DateTime.fromMicrosecondsSinceEpoch( + lastSyncedAt, + ), }, ); } @@ -116,12 +124,11 @@ final class DownloadProgress { factory DownloadProgress.fromJson(Map line) { final rawBuckets = line['buckets'] as Map; - return DownloadProgress(rawBuckets.map((k, v) { - return MapEntry( - k, - _bucketProgressFromJson(v as Map), - ); - })); + return DownloadProgress( + rawBuckets.map((k, v) { + return MapEntry(k, _bucketProgressFromJson(v as Map)); + }), + ); } Map toJson() { @@ -132,9 +139,9 @@ final class DownloadProgress { 'priority': value.priority.priorityNumber, 'at_last': value.atLast, 'since_last': value.sinceLast, - 'target_count': value.targetCount + 'target_count': value.targetCount, }, - } + }, }; } diff --git a/packages/powersync/lib/src/sync/internal_connector.dart b/packages/powersync/lib/src/sync/internal_connector.dart index a18914c7..0bbe0d36 100644 --- a/packages/powersync/lib/src/sync/internal_connector.dart +++ b/packages/powersync/lib/src/sync/internal_connector.dart @@ -25,12 +25,14 @@ abstract interface class InternalConnector { const factory InternalConnector({ required Future Function() getCredentialsCached, required Future Function({required bool invalidate}) - prefetchCredentials, + prefetchCredentials, required Future Function() uploadCrud, }) = _CallbackConnector; factory InternalConnector.wrap( - PowerSyncBackendConnector connector, PowerSyncDatabase db) { + PowerSyncBackendConnector connector, + PowerSyncDatabase db, + ) { return _WrapConnector(connector, db); } } @@ -63,17 +65,17 @@ final class _WrapConnector implements InternalConnector { final class _CallbackConnector implements InternalConnector { final Future Function() _getCredentialsCached; final Future Function({required bool invalidate}) - _prefetchCredentials; + _prefetchCredentials; final Future Function() _uploadCrud; const _CallbackConnector({ required Future Function() getCredentialsCached, required Future Function({required bool invalidate}) - prefetchCredentials, + prefetchCredentials, required Future Function() uploadCrud, - }) : _getCredentialsCached = getCredentialsCached, - _prefetchCredentials = prefetchCredentials, - _uploadCrud = uploadCrud; + }) : _getCredentialsCached = getCredentialsCached, + _prefetchCredentials = prefetchCredentials, + _uploadCrud = uploadCrud; @override Future getCredentialsCached() { diff --git a/packages/powersync/lib/src/sync/options.dart b/packages/powersync/lib/src/sync/options.dart index 7ef155ce..da7f14e1 100644 --- a/packages/powersync/lib/src/sync/options.dart +++ b/packages/powersync/lib/src/sync/options.dart @@ -112,12 +112,14 @@ extension type ResolvedSyncOptions(SyncOptions source) { Map? params, Map? appMetadata, }) { - return ResolvedSyncOptions((source ?? SyncOptions())._copyWith( - crudThrottleTime: crudThrottleTime, - retryDelay: retryDelay, - params: params, - appMetadata: appMetadata, - )); + return ResolvedSyncOptions( + (source ?? SyncOptions())._copyWith( + crudThrottleTime: crudThrottleTime, + retryDelay: retryDelay, + params: params, + appMetadata: appMetadata, + ), + ); } Map get appMetadata => source.appMetadata ?? const {}; @@ -145,7 +147,8 @@ extension type ResolvedSyncOptions(SyncOptions source) { httpClient: other.httpClient ?? source.httpClient, ); - final didChange = !_mapEquality.equals(newOptions.params, params) || + final didChange = + !_mapEquality.equals(newOptions.params, params) || newOptions.crudThrottleTime != crudThrottleTime || newOptions.retryDelay != retryDelay || newOptions.syncImplementation != source.syncImplementation || diff --git a/packages/powersync/lib/src/sync/stream.dart b/packages/powersync/lib/src/sync/stream.dart index f53583b7..3a7817fb 100644 --- a/packages/powersync/lib/src/sync/stream.dart +++ b/packages/powersync/lib/src/sync/stream.dart @@ -135,13 +135,15 @@ final class CoreActiveStreamSubscription hasExplicitSubscription: json['has_explicit_subscription'] as bool, expiresAt: switch (json['expires_at']) { null => null, - final timestamp as int => - DateTime.fromMicrosecondsSinceEpoch(timestamp), + final timestamp as int => DateTime.fromMicrosecondsSinceEpoch( + timestamp, + ), }, lastSyncedAt: switch (json['last_synced_at']) { null => null, - final timestamp as int => - DateTime.fromMicrosecondsSinceEpoch(timestamp), + final timestamp as int => DateTime.fromMicrosecondsSinceEpoch( + timestamp, + ), }, ); } @@ -151,10 +153,7 @@ final class CoreActiveStreamSubscription 'name': name, 'parameters': parameters, 'priority': priority.priorityNumber, - 'progress': { - 'total': progress.total, - 'downloaded': progress.downloaded, - }, + 'progress': {'total': progress.total, 'downloaded': progress.downloaded}, 'active': active, 'is_default': isDefault, 'has_explicit_subscription': hasExplicitSubscription, @@ -165,12 +164,13 @@ final class CoreActiveStreamSubscription 'last_synced_at': switch (lastSyncedAt) { null => null, final lastSyncedAt => lastSyncedAt.microsecondsSinceEpoch, - } + }, }; } static ({int total, int downloaded}) _progressFromJson( - Map json) { + Map json, + ) { return (total: json['total'] as int, downloaded: json['downloaded'] as int); } } diff --git a/packages/powersync/lib/src/sync/stream_utils.dart b/packages/powersync/lib/src/sync/stream_utils.dart index 65b6f891..feba0a37 100644 --- a/packages/powersync/lib/src/sync/stream_utils.dart +++ b/packages/powersync/lib/src/sync/stream_utils.dart @@ -139,21 +139,24 @@ Stream streamFromFutureAwaitInCancellation(Future future) { final controller = StreamController(sync: true); var cancelled = false; - final handledFuture = future.then((value) { - controller - ..add(value) - ..close(); - }, onError: (Object error, StackTrace trace) { - if (cancelled) { - // Make handledFuture complete with the error, so that controller.cancel - // throws (instead of the error being unhandled). - throw error; - } else { + final handledFuture = future.then( + (value) { controller - ..addError(error, trace) + ..add(value) ..close(); - } - }); + }, + onError: (Object error, StackTrace trace) { + if (cancelled) { + // Make handledFuture complete with the error, so that controller.cancel + // throws (instead of the error being unhandled). + throw error; + } else { + controller + ..addError(error, trace) + ..close(); + } + }, + ); controller.onCancel = () async { cancelled = true; @@ -191,8 +194,12 @@ final class _BsonSplittingSink implements EventSink> { assert(remainingBytes >= 0); if (remainingBytes == 0) { - _downstream.add(pending.buffer - .asUint8List(pending.offsetInBytes, pending.lengthInBytes)); + _downstream.add( + pending.buffer.asUint8List( + pending.offsetInBytes, + pending.lengthInBytes, + ), + ); // Prepare reading another document, starting with its length pendingBuffer = null; @@ -220,7 +227,8 @@ final class _BsonSplittingSink implements EventSink> { if (remainingBytes < 5) { _downstream.addError( PowerSyncProtocolException( - 'Invalid length for bson: $remainingBytes'), + 'Invalid length for bson: $remainingBytes', + ), StackTrace.current, ); } diff --git a/packages/powersync/lib/src/sync/streaming_sync.dart b/packages/powersync/lib/src/sync/streaming_sync.dart index e0bfebed..9e13e4c5 100644 --- a/packages/powersync/lib/src/sync/streaming_sync.dart +++ b/packages/powersync/lib/src/sync/streaming_sync.dart @@ -81,12 +81,12 @@ class StreamingSyncImplementation implements StreamingSync { /// A unique identifier for this streaming sync implementation /// A good value is typically the DB file path which it will mutate when syncing. String? identifier = "unknown", - }) : _client = options.createHttpClient(), - syncMutex = syncMutex ?? potentiallySharedMutex("sync-$identifier"), - crudMutex = crudMutex ?? potentiallySharedMutex("crud-$identifier"), - _userAgentHeaders = userAgentHeaders(), - logger = logger ?? isolateLogger, - _activeSubscriptions = activeSubscriptions; + }) : _client = options.createHttpClient(), + syncMutex = syncMutex ?? potentiallySharedMutex("sync-$identifier"), + crudMutex = crudMutex ?? potentiallySharedMutex("crud-$identifier"), + _userAgentHeaders = userAgentHeaders(), + logger = logger ?? isolateLogger, + _activeSubscriptions = activeSubscriptions; Duration get _retryDelay => options.retryDelay; @@ -199,8 +199,10 @@ class StreamingSyncImplementation implements StreamingSync { // but it should not result in excessive uploads since the // sync reconnects are also throttled. // The stream here is closed on abort. - await for (var _ in mergeStreams( - [crudUpdateTriggerStream, _internalCrudTriggerController.stream])) { + await for (var _ in mergeStreams([ + crudUpdateTriggerStream, + _internalCrudTriggerController.stream, + ])) { await _uploadAllCrud(); } } @@ -208,69 +210,75 @@ class StreamingSyncImplementation implements StreamingSync { Future _uploadAllCrud() { assert(_activeCrudUpload == null); final completer = _activeCrudUpload = Completer(); - return crudMutex.lock(() async { - // Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration. - CrudEntry? checkedCrudItem; - - while (true) { - try { - // It's possible that an abort or disconnect operation could - // be followed by a `close` operation. The close would cause these - // operations, which use the DB, to throw an exception. Breaking the loop - // here prevents unnecessary potential (caught) exceptions. - if (aborted) { - break; - } - // This is the first item in the FIFO CRUD queue. - CrudEntry? nextCrudItem = await adapter.nextCrudItem(); - if (nextCrudItem != null) { - _state.updateStatus((s) => s.uploading = true); - if (nextCrudItem.clientId == checkedCrudItem?.clientId) { - // This will force a higher log level than exceptions which are caught here. - logger.warning( - """Potentially previously uploaded CRUD entries are still present in the upload queue. + return crudMutex + .lock(() async { + // Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration. + CrudEntry? checkedCrudItem; + + while (true) { + try { + // It's possible that an abort or disconnect operation could + // be followed by a `close` operation. The close would cause these + // operations, which use the DB, to throw an exception. Breaking the loop + // here prevents unnecessary potential (caught) exceptions. + if (aborted) { + break; + } + // This is the first item in the FIFO CRUD queue. + CrudEntry? nextCrudItem = await adapter.nextCrudItem(); + if (nextCrudItem != null) { + _state.updateStatus((s) => s.uploading = true); + if (nextCrudItem.clientId == checkedCrudItem?.clientId) { + // This will force a higher log level than exceptions which are caught here. + logger.warning( + """Potentially previously uploaded CRUD entries are still present in the upload queue. Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. - The next upload iteration will be delayed."""); - throw Exception( - 'Delaying due to previously encountered CRUD item.'); + The next upload iteration will be delayed.""", + ); + throw Exception( + 'Delaying due to previously encountered CRUD item.', + ); + } + + checkedCrudItem = nextCrudItem; + await connector.uploadCrud(); + _state.updateStatus((s) => s.uploadError = null); + } else { + // Uploading is completed + await adapter.updateTargetCheckpointRequest( + () => getWriteCheckpoint(), + ); + break; + } + } catch (e, stacktrace) { + checkedCrudItem = null; + logger.warning('Data upload error', e, stacktrace); + _state.updateStatus((s) => s.applyUploadError(e)); + await _delayRetry(); + + if (!_state.status.connected) { + // Exit the upload loop if the sync stream is no longer connected + break; + } + logger.warning( + "Caught exception when uploading. Upload will retry after a delay", + e, + stacktrace, + ); + } finally { + _state.updateStatus((s) => s.uploading = false); } - - checkedCrudItem = nextCrudItem; - await connector.uploadCrud(); - _state.updateStatus((s) => s.uploadError = null); - } else { - // Uploading is completed - await adapter - .updateTargetCheckpointRequest(() => getWriteCheckpoint()); - break; } - } catch (e, stacktrace) { - checkedCrudItem = null; - logger.warning('Data upload error', e, stacktrace); - _state.updateStatus((s) => s.applyUploadError(e)); - await _delayRetry(); - - if (!_state.status.connected) { - // Exit the upload loop if the sync stream is no longer connected - break; + }, abortTrigger: _delayRetry()) + .whenComplete(() { + if (!aborted) { + _nonLineSyncEvents.add(const UploadCompleted()); } - logger.warning( - "Caught exception when uploading. Upload will retry after a delay", - e, - stacktrace); - } finally { - _state.updateStatus((s) => s.uploading = false); - } - } - }, abortTrigger: _delayRetry()).whenComplete(() { - if (!aborted) { - _nonLineSyncEvents.add(const UploadCompleted()); - } - assert(identical(_activeCrudUpload, completer)); - _activeCrudUpload = null; - completer.complete(); - }); + assert(identical(_activeCrudUpload, completer)); + _activeCrudUpload = null; + completer.complete(); + }); } Future getWriteCheckpoint() async { @@ -278,13 +286,14 @@ class StreamingSyncImplementation implements StreamingSync { if (credentials == null) { throw CredentialsException("Not logged in"); } - final uri = - credentials.endpointUri('write-checkpoint2.json?client_id=$clientId'); + final uri = credentials.endpointUri( + 'write-checkpoint2.json?client_id=$clientId', + ); Map headers = { 'Content-Type': 'application/json', 'Authorization': "Token ${credentials.token}", - ..._userAgentHeaders + ..._userAgentHeaders, }; final response = await _client.get(uri, headers: headers); @@ -300,17 +309,23 @@ class StreamingSyncImplementation implements StreamingSync { } Future _rustStreamingSyncIteration( - AbortController abortController) async { + AbortController abortController, + ) async { logger.info('Starting Rust sync iteration'); - final response = await _ActiveRustStreamingIteration(this, abortController) - .syncIteration(); + final response = await _ActiveRustStreamingIteration( + this, + abortController, + ).syncIteration(); logger.info( - 'Ending Rust sync iteration. Immediate restart: ${response.immediateRestart}'); + 'Ending Rust sync iteration. Immediate restart: ${response.immediateRestart}', + ); return response; } - Future _postStreamRequest(Object? data, - {Future? onAbort}) async { + Future _postStreamRequest( + Object? data, { + Future? onAbort, + }) async { const ndJson = 'application/x-ndjson'; const bson = 'application/vnd.powersync.bson-stream'; @@ -320,8 +335,11 @@ class StreamingSyncImplementation implements StreamingSync { } final uri = credentials.endpointUri('sync/stream'); - final request = http.AbortableRequest('POST', uri, - abortTrigger: onAbort ?? _abort!.onAbort); + final request = http.AbortableRequest( + 'POST', + uri, + abortTrigger: onAbort ?? _abort!.onAbort, + ); request.headers['Content-Type'] = 'application/json'; request.headers['Authorization'] = "Token ${credentials.token}"; request.headers['Accept'] = '$bson;q=0.9,$ndJson;q=0.8'; @@ -391,10 +409,7 @@ String _syncErrorMessage(Object? error) { } } -typedef BucketDescription = ({ - String name, - int priority, -}); +typedef BucketDescription = ({String name, int priority}); final class _ActiveRustStreamingIteration { final StreamingSyncImplementation sync; @@ -407,8 +422,9 @@ final class _ActiveRustStreamingIteration { List _encodeSubscriptions(List subscriptions) { return sync._activeSubscriptions - .map((s) => - {'name': s.name, 'params': convert.json.decode(s.parameters)}) + .map( + (s) => {'name': s.name, 'params': convert.json.decode(s.parameters)}, + ) .toList(); } @@ -454,8 +470,8 @@ final class _ActiveRustStreamingIteration { Stream _receiveLines(Object? data) { return streamFromFutureAwaitInCancellation( - sync._postStreamRequest(data, onAbort: _abortController.onAbort)) - .asyncExpand((response) async* { + sync._postStreamRequest(data, onAbort: _abortController.onAbort), + ).asyncExpand((response) async* { if (response == null) { return; } else { @@ -554,8 +570,10 @@ final class _ActiveRustStreamingIteration { } } - Future> _invokePowerSyncControl(String operation, - [Object? payload]) async { + Future> _invokePowerSyncControl( + String operation, [ + Object? payload, + ]) async { final rawResponse = await sync.adapter.control(operation, payload); final instructions = convert.json.decode(rawResponse) as List; @@ -563,29 +581,31 @@ final class _ActiveRustStreamingIteration { } Future _handleInstruction( - NonInterruptingInstruction instruction) async { + NonInterruptingInstruction instruction, + ) async { switch (instruction) { case LogLine(:final severity, :final line): - sync.logger.log( - switch (severity) { - 'DEBUG' => Level.FINE, - 'INFO' => Level.INFO, - _ => Level.WARNING, - }, - line); + sync.logger.log(switch (severity) { + 'DEBUG' => Level.FINE, + 'INFO' => Level.INFO, + _ => Level.WARNING, + }, line); case UpdateSyncStatus(:final status): sync._state.updateStatus((m) => m.applyFromCore(status)); case FetchCredentials(:final didExpire): if (didExpire) { await sync.connector.prefetchCredentials(invalidate: true); } else { - sync.connector.prefetchCredentials().then((_) { - if (!sync.aborted) { - sync._nonLineSyncEvents.add(const TokenRefreshComplete()); - } - }, onError: (Object e, StackTrace s) { - sync.logger.warning('Could not prefetch credentials', e, s); - }); + sync.connector.prefetchCredentials().then( + (_) { + if (!sync.aborted) { + sync._nonLineSyncEvents.add(const TokenRefreshComplete()); + } + }, + onError: (Object e, StackTrace s) { + sync.logger.warning('Could not prefetch credentials', e, s); + }, + ); } case DidCompleteSync(): sync._state.updateStatus((m) => m.downloadError = null); @@ -599,10 +619,7 @@ typedef RustSyncIterationResult = ({bool immediateRestart}); sealed class SyncEvent {} -enum ConnectionEvent implements SyncEvent { - established, - end, -} +enum ConnectionEvent implements SyncEvent { established, end } final class ReceivedLine implements SyncEvent { final Object /* String|Uint8List|StreamingSyncLine */ line; diff --git a/packages/powersync/lib/src/sync/sync_status.dart b/packages/powersync/lib/src/sync/sync_status.dart index 7f58a915..99d7a457 100644 --- a/packages/powersync/lib/src/sync/sync_status.dart +++ b/packages/powersync/lib/src/sync/sync_status.dart @@ -68,22 +68,22 @@ final class SyncStatus { required this.uploadError, required this.priorityStatusEntries, required List? streamSubscriptions, - }) : hasSynced = lastSyncedAt != null, - _internalSubscriptions = streamSubscriptions; + }) : hasSynced = lastSyncedAt != null, + _internalSubscriptions = streamSubscriptions; @internal const SyncStatus.uninitialized() - : connected = false, - connecting = false, - lastSyncedAt = null, - downloadProgress = null, - downloading = false, - uploading = false, - uploadError = null, - downloadError = null, - priorityStatusEntries = const [], - _internalSubscriptions = null, - hasSynced = null; + : connected = false, + connecting = false, + lastSyncedAt = null, + downloadProgress = null, + downloading = false, + uploading = false, + uploadError = null, + downloadError = null, + priorityStatusEntries = const [], + _internalSubscriptions = null, + hasSynced = null; @override bool operator ==(Object other) { @@ -97,9 +97,13 @@ final class SyncStatus { other.lastSyncedAt == lastSyncedAt && other.hasSynced == hasSynced && _listEquality.equals( - other.priorityStatusEntries, priorityStatusEntries) && + other.priorityStatusEntries, + priorityStatusEntries, + ) && _listEquality.equals( - other._internalSubscriptions, _internalSubscriptions) && + other._internalSubscriptions, + _internalSubscriptions, + ) && other.downloadProgress == downloadProgress); } @@ -159,8 +163,12 @@ final class SyncStatus { /// in priority `2` necessarily includes a consistent view over data in /// priority `1`. SyncPriorityStatus statusForPriority(StreamPriority priority) { - assert(priorityStatusEntries.isSortedByCompare( - (e) => e.priority, StreamPriority.comparator)); + assert( + priorityStatusEntries.isSortedByCompare( + (e) => e.priority, + StreamPriority.comparator, + ), + ); for (final known in priorityStatusEntries) { // Lower-priority buckets are synchronized after higher-priority buckets, @@ -175,7 +183,7 @@ final class SyncStatus { return ( priority: priority, hasSynced: hasSynced, - lastSyncedAt: lastSyncedAt + lastSyncedAt: lastSyncedAt, ); } @@ -259,8 +267,10 @@ extension InternalSyncStatusAccess on SyncStatus { List? get internalSubscriptions => _internalSubscriptions; - SyncStatus changeErrors( - {required Object? downloadError, required Object? uploadError}) { + SyncStatus changeErrors({ + required Object? downloadError, + required Object? uploadError, + }) { return SyncStatus( connected: connected, connecting: connecting, @@ -294,7 +304,7 @@ final class SyncStreamStatus { StreamPriority get priority => _internal.priority; SyncStreamStatus._(this._internal, SyncDownloadProgress? progress) - : progress = progress?._internal._forStream(_internal); + : progress = progress?._internal._forStream(_internal); } @Deprecated('Use StreamPriority instead') @@ -364,10 +374,10 @@ final class InternalSyncDownloadProgress extends ProgressWithOperations { final Map buckets; InternalSyncDownloadProgress(this.buckets) - : super._( - buckets.values.map((e) => e.targetCount - e.atLast).sum, - buckets.values.map((e) => e.sinceLast).sum, - ); + : super._( + buckets.values.map((e) => e.targetCount - e.atLast).sum, + buckets.values.map((e) => e.sinceLast).sum, + ); static InternalSyncDownloadProgress ofPublic(SyncDownloadProgress public) { return public._internal; diff --git a/packages/powersync/lib/src/web/http/server.dart b/packages/powersync/lib/src/web/http/server.dart index c9e418ea..c9e24804 100644 --- a/packages/powersync/lib/src/web/http/server.dart +++ b/packages/powersync/lib/src/web/http/server.dart @@ -24,8 +24,11 @@ final class RemoteHttpServer { final state = _HttpRequest(); _pendingTransactions[request.transactionId] = state; - final inner = AbortableRequest(request.method, Uri.parse(request.uri), - abortTrigger: state._abortController.future); + final inner = AbortableRequest( + request.method, + Uri.parse(request.uri), + abortTrigger: state._abortController.future, + ); inner.bodyBytes = request.body.toDart.asUint8List(); request.decodedHeaders.forEach((k, v) => inner.headers[k] = v); diff --git a/packages/powersync/lib/src/web/sync_controller.dart b/packages/powersync/lib/src/web/sync_controller.dart index 66854748..fea79335 100644 --- a/packages/powersync/lib/src/web/sync_controller.dart +++ b/packages/powersync/lib/src/web/sync_controller.dart @@ -50,7 +50,7 @@ class SyncWorkerHandle implements StreamingSync { databasePort: endpoint.connectPort, lockName: endpoint.lockName, ), - [endpoint.connectPort].toJS + [endpoint.connectPort].toJS, ); case SyncWorkerMessageType.uploadCrud: await connector.uploadData(database); @@ -61,7 +61,7 @@ class SyncWorkerHandle implements StreamingSync { credentials != null ? SerializedCredentials.from(credentials) : null, - null + null, ); case SyncWorkerMessageType.credentialsCallback: final credentials = await connector.getCredentialsCached(); @@ -69,7 +69,7 @@ class SyncWorkerHandle implements StreamingSync { credentials != null ? SerializedCredentials.from(credentials) : null, - null + null, ); default: throw StateError('Unexpected message type $type'); @@ -100,10 +100,7 @@ class SyncWorkerHandle implements StreamingSync { // the shared worker. worker.port.start(); worker.port.postMessage( - SharedWorkerMessage( - isForSyncWorker: true, - message: port2, - ), + SharedWorkerMessage(isForSyncWorker: true, message: port2), [port2].toJS, ); diff --git a/packages/powersync/lib/src/web/sync_worker.dart b/packages/powersync/lib/src/web/sync_worker.dart index 79364c9c..0a13d68d 100644 --- a/packages/powersync/lib/src/web/sync_worker.dart +++ b/packages/powersync/lib/src/web/sync_worker.dart @@ -36,20 +36,15 @@ class SyncWorker { } SyncRunner _referenceSyncTask( - String databaseIdentifier, - SyncOptions options, - String schemaJson, - List subscriptions, - ConnectedClient client) { + String databaseIdentifier, + SyncOptions options, + String schemaJson, + List subscriptions, + ConnectedClient client, + ) { return requestedSyncTasks.putIfAbsent(databaseIdentifier, () { return SyncRunner(databaseIdentifier); - }) - ..registerClient( - client, - options, - schemaJson, - subscriptions, - ); + })..registerClient(client, options, schemaJson, subscriptions); } } @@ -71,8 +66,9 @@ class ConnectedClient { channel.observeRemoteLockName(request.lockName); final recoveredOptions = SyncOptions( - crudThrottleTime: - Duration(milliseconds: request.crudThrottleTimeMs), + crudThrottleTime: Duration( + milliseconds: request.crudThrottleTimeMs, + ), retryDelay: switch (request.retryDelayMs) { null => null, final retryDelay => Duration(milliseconds: retryDelay), @@ -89,7 +85,8 @@ class ConnectedClient { appMetadata: switch (request.appMetadataEncoded) { null => null, final encodedAppMetadata => Map.from( - jsonDecode(encodedAppMetadata) as Map), + jsonDecode(encodedAppMetadata) as Map, + ), }, httpClient: request.customHttpClient == true ? () => RemoteHttpClient(channel) @@ -110,7 +107,9 @@ class ConnectedClient { return (JSObject(), null); case SyncWorkerMessageType.updateSubscriptions: _runner?.updateClientSubscriptions( - this, (payload as UpdateSubscriptions).toDart); + this, + (payload as UpdateSubscriptions).toDart, + ); return (JSObject(), null); default: throw StateError('Unexpected message type $type'); @@ -121,7 +120,8 @@ class ConnectedClient { _logSubscription = _logger.onRecord.listen((record) { final msg = StringBuffer( - '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}', + ); if (record.error != null) { msg @@ -172,11 +172,11 @@ class SyncRunner { try { switch (event) { case _AddConnection( - :final client, - :final options, - :final schemaJson, - :final subscriptions, - ): + :final client, + :final options, + :final schemaJson, + :final subscriptions, + ): connections[client] = subscriptions; final (newOptions, reconnect) = this.options.applyFrom(options); this.options = newOptions; @@ -210,9 +210,9 @@ class SyncRunner { await sync?.abort(); sync = null; case _ClientSubscriptionsChanged( - :final client, - :final subscriptions - ): + :final client, + :final subscriptions, + ): connections[client] = subscriptions; reindexSubscriptions(); } @@ -242,7 +242,8 @@ class SyncRunner { final after = connections.values.flattenedToSet; if (!const SetEquality().equals(before, after)) { _logger.info( - 'Subscriptions across tabs have changed, checking whether a reconnect is necessary'); + 'Subscriptions across tabs have changed, checking whether a reconnect is necessary', + ); currentStreams = after.toList(); sync?.updateSubscriptions(currentStreams); } @@ -262,19 +263,25 @@ class SyncRunner { var pendingRequests = candidates.length; for (final candidate in candidates) { - candidate.channel.ping().then((_) { - pendingRequests--; - if (!firstResponder.isCompleted) { - firstResponder.complete(candidate); - } - }).timeout(const Duration(seconds: 5), onTimeout: () { - pendingRequests--; - candidate.markClosed(); - if (pendingRequests == 0 && !firstResponder.isCompleted) { - // All requests have timed out, no connection remains - firstResponder.complete(null); - } - }); + candidate.channel + .ping() + .then((_) { + pendingRequests--; + if (!firstResponder.isCompleted) { + firstResponder.complete(candidate); + } + }) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + pendingRequests--; + candidate.markClosed(); + if (pendingRequests == 0 && !firstResponder.isCompleted) { + // All requests have timed out, no connection remains + firstResponder.complete(null); + } + }, + ); } return firstResponder.future; @@ -300,10 +307,12 @@ class SyncRunner { }); final tables = ['ps_crud']; - Stream crudStream = - powerSyncUpdateNotifications(Stream.empty()); - final filteredStream = database.updates - .transform(UpdateNotification.filterTablesTransformer(tables)); + Stream crudStream = powerSyncUpdateNotifications( + Stream.empty(), + ); + final filteredStream = database.updates.transform( + UpdateNotification.filterTablesTransformer(tables), + ); crudStream = UpdateNotification.throttleStream( filteredStream, options.crudThrottleTime, @@ -339,8 +348,12 @@ class SyncRunner { sync!.streamingSync(); } - void registerClient(ConnectedClient client, SyncOptions options, - String schemaJson, List subscriptions) { + void registerClient( + ConnectedClient client, + SyncOptions options, + String schemaJson, + List subscriptions, + ) { _mainEvents.add(_AddConnection(client, options, schemaJson, subscriptions)); } @@ -355,7 +368,9 @@ class SyncRunner { } void updateClientSubscriptions( - ConnectedClient client, List subscriptions) { + ConnectedClient client, + List subscriptions, + ) { _mainEvents.add(_ClientSubscriptionsChanged(client, subscriptions)); } } @@ -369,7 +384,11 @@ final class _AddConnection implements _RunnerEvent { final List subscriptions; _AddConnection( - this.client, this.options, this.schemaJson, this.subscriptions); + this.client, + this.options, + this.schemaJson, + this.subscriptions, + ); } final class _RemoveConnection implements _RunnerEvent { diff --git a/packages/powersync/lib/src/web/sync_worker_protocol.dart b/packages/powersync/lib/src/web/sync_worker_protocol.dart index c341747a..79a09be9 100644 --- a/packages/powersync/lib/src/web/sync_worker_protocol.dart +++ b/packages/powersync/lib/src/web/sync_worker_protocol.dart @@ -84,8 +84,10 @@ enum SyncWorkerMessageType { @anonymous extension type SyncWorkerMessage._(JSObject _) implements JSObject { - external factory SyncWorkerMessage( - {required String type, required JSAny payload}); + external factory SyncWorkerMessage({ + required String type, + required JSAny payload, + }); external String get type; external JSAny get payload; @@ -241,7 +243,8 @@ extension type SerializedBucketProgress._(JSObject _) implements JSObject { external int targetCount; static JSArray serialize( - Map buckets) { + Map buckets, + ) { return [ for (final MapEntry(:key, :value) in buckets.entries) SerializedBucketProgress( @@ -255,7 +258,8 @@ extension type SerializedBucketProgress._(JSObject _) implements JSObject { } static Map deserialize( - JSArray array) { + JSArray array, + ) { return { for (final entry in array.toDart) entry.name: ( @@ -300,12 +304,13 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { entry.priority.priorityNumber.toJS, entry.lastSyncedAt?.microsecondsSinceEpoch.toJS, entry.hasSynced?.toJS, - ].toJS + ].toJS, ].toJS, syncProgress: switch (status.downloadProgress) { null => null, var other => SerializedBucketProgress.serialize( - InternalSyncDownloadProgress.ofPublic(other).buckets), + InternalSyncDownloadProgress.ofPublic(other).buckets, + ), }, streamSubscriptions: json.encode(status.internalSubscriptions).toJS, ); @@ -351,20 +356,24 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { : null, hasSynced: (rawHasSynced as JSBoolean?)?.toDart, ); - }) + }), ], downloadProgress: switch (syncProgress) { null => null, final serializedProgress => InternalSyncDownloadProgress( - SerializedBucketProgress.deserialize(serializedProgress)) - .asSyncDownloadProgress, + SerializedBucketProgress.deserialize(serializedProgress), + ).asSyncDownloadProgress, }, streamSubscriptions: switch (streamSubscriptions) { null => null, - final serialized => (json.decode(serialized) as List?) - ?.map((e) => CoreActiveStreamSubscription.fromJson( - e as Map)) - .toList(), + final serialized => + (json.decode(serialized) as List?) + ?.map( + (e) => CoreActiveStreamSubscription.fromJson( + e as Map, + ), + ) + .toList(), }, ); } @@ -386,7 +395,7 @@ final class WorkerCommunicationChannel { final MessagePort port; final FutureOr<(JSAny?, JSArray?)> Function(SyncWorkerMessageType, JSAny) - requestHandler; + requestHandler; final StreamController<(SyncWorkerMessageType, JSAny)> _events = StreamController(); final Logger _logger; @@ -402,10 +411,10 @@ final class WorkerCommunicationChannel { Stream? errors, Logger? logger, Client? exposedHttpClient, - }) : _logger = logger ?? autoLogger, - _httpServer = exposedHttpClient == null - ? null - : RemoteHttpServer(exposedHttpClient) { + }) : _logger = logger ?? autoLogger, + _httpServer = exposedHttpClient == null + ? null + : RemoteHttpServer(exposedHttpClient) { port.start(); _incomingErrors = errors?.listen((event) { _hasError = true; @@ -416,75 +425,86 @@ final class WorkerCommunicationChannel { _pendingRequests.clear(); }); - _incomingMessages = - EventStreamProviders.messageEvent.forTarget(port).listen((event) async { - final message = event.data as SyncWorkerMessage; - final type = SyncWorkerMessageType.values.byName(message.type); - _logger.fine('[in] $type'); - - int requestId; - - switch (type) { - case SyncWorkerMessageType.ping: - requestId = (message.payload as JSNumber).toDartInt; - return _respond(requestId, () async => (null, null)); - case SyncWorkerMessageType.startSynchronization: - requestId = (message.payload as StartSynchronization).requestId; - case SyncWorkerMessageType.updateSubscriptions: - requestId = (message.payload as UpdateSubscriptions).requestId; - case SyncWorkerMessageType.requestEndpoint: - case SyncWorkerMessageType.abortSynchronization: - case SyncWorkerMessageType.credentialsCallback: - case SyncWorkerMessageType.invalidCredentialsCallback: - case SyncWorkerMessageType.uploadCrud: - requestId = (message.payload as JSNumber).toDartInt; - case SyncWorkerMessageType.sendHttpRequest: - final request = message.payload as HttpRequest; - return _respond(request.requestId, - () async => (await _httpServer!.handle(request), null)); - case SyncWorkerMessageType.abortHttpRequest: - final payload = message.payload as AbortHttpResponse; - _httpServer!.abort(payload.transactionId, payload.cancelStream); - return; - case SyncWorkerMessageType.readResponseChunk: - final request = message.payload as ReadStreamChunk; - return _respond(request.requestId, () async { - return switch ( - await _httpServer!.readResponse(request.transactionId)) { - null => (null, null), - final buffer => (buffer, [buffer].toJS), - }; - }); - case SyncWorkerMessageType.okResponse: - final payload = message.payload as OkResponse; - _pendingRequests.remove(payload.requestId)!.complete(payload.payload); - return; - case SyncWorkerMessageType.errorResponse: - final payload = message.payload as ErrorResponse; - final error = switch ( - payload.recognizedType ?? ErrorResponse.recognizedTypeNone) { - ErrorResponse.recognizedTypeRequestAbortedException => - RequestAbortedException(), - _ => payload.errorMessage.toDart, - }; - - _pendingRequests.remove(payload.requestId)!.completeError(error); - return; - case SyncWorkerMessageType.notifySyncStatus: - _events.add((type, message.payload)); - return; - case SyncWorkerMessageType.logEvent: - final msg = (message.payload as JSString).toDart; - _logger.info('[Sync Worker]: $msg'); - return; - } - - await _respond(requestId, () => requestHandler(type, message.payload)); - }); + _incomingMessages = EventStreamProviders.messageEvent + .forTarget(port) + .listen((event) async { + final message = event.data as SyncWorkerMessage; + final type = SyncWorkerMessageType.values.byName(message.type); + _logger.fine('[in] $type'); + + int requestId; + + switch (type) { + case SyncWorkerMessageType.ping: + requestId = (message.payload as JSNumber).toDartInt; + return _respond(requestId, () async => (null, null)); + case SyncWorkerMessageType.startSynchronization: + requestId = (message.payload as StartSynchronization).requestId; + case SyncWorkerMessageType.updateSubscriptions: + requestId = (message.payload as UpdateSubscriptions).requestId; + case SyncWorkerMessageType.requestEndpoint: + case SyncWorkerMessageType.abortSynchronization: + case SyncWorkerMessageType.credentialsCallback: + case SyncWorkerMessageType.invalidCredentialsCallback: + case SyncWorkerMessageType.uploadCrud: + requestId = (message.payload as JSNumber).toDartInt; + case SyncWorkerMessageType.sendHttpRequest: + final request = message.payload as HttpRequest; + return _respond( + request.requestId, + () async => (await _httpServer!.handle(request), null), + ); + case SyncWorkerMessageType.abortHttpRequest: + final payload = message.payload as AbortHttpResponse; + _httpServer!.abort(payload.transactionId, payload.cancelStream); + return; + case SyncWorkerMessageType.readResponseChunk: + final request = message.payload as ReadStreamChunk; + return _respond(request.requestId, () async { + return switch (await _httpServer!.readResponse( + request.transactionId, + )) { + null => (null, null), + final buffer => (buffer, [buffer].toJS), + }; + }); + case SyncWorkerMessageType.okResponse: + final payload = message.payload as OkResponse; + _pendingRequests + .remove(payload.requestId)! + .complete(payload.payload); + return; + case SyncWorkerMessageType.errorResponse: + final payload = message.payload as ErrorResponse; + final error = switch (payload.recognizedType ?? + ErrorResponse.recognizedTypeNone) { + ErrorResponse.recognizedTypeRequestAbortedException => + RequestAbortedException(), + _ => payload.errorMessage.toDart, + }; + + _pendingRequests.remove(payload.requestId)!.completeError(error); + return; + case SyncWorkerMessageType.notifySyncStatus: + _events.add((type, message.payload)); + return; + case SyncWorkerMessageType.logEvent: + final msg = (message.payload as JSString).toDart; + _logger.info('[Sync Worker]: $msg'); + return; + } + + await _respond( + requestId, + () => requestHandler(type, message.payload), + ); + }); } - Future _respond(int requestId, - FutureOr<(JSAny?, JSArray?)> Function() generateResponse) async { + Future _respond( + int requestId, + FutureOr<(JSAny?, JSArray?)> Function() generateResponse, + ) async { try { final (response, transfer) = await generateResponse(); final responseMessage = SyncWorkerMessage( @@ -498,18 +518,20 @@ final class WorkerCommunicationChannel { port.postMessage(responseMessage); } } catch (e) { - port.postMessage(SyncWorkerMessage( - type: SyncWorkerMessageType.errorResponse.name, - payload: ErrorResponse( - requestId: requestId, - recognizedType: switch (e) { - RequestAbortedException() => - ErrorResponse.recognizedTypeRequestAbortedException, - _ => ErrorResponse.recognizedTypeNone, - }, - errorMessage: e.toString().toJS, + port.postMessage( + SyncWorkerMessage( + type: SyncWorkerMessageType.errorResponse.name, + payload: ErrorResponse( + requestId: requestId, + recognizedType: switch (e) { + RequestAbortedException() => + ErrorResponse.recognizedTypeRequestAbortedException, + _ => ErrorResponse.recognizedTypeNone, + }, + errorMessage: e.toString().toJS, + ), ), - )); + ); } } @@ -531,7 +553,8 @@ final class WorkerCommunicationChannel { void notify(SyncWorkerMessageType notificationType, JSAny payload) { port.postMessage( - SyncWorkerMessage(type: notificationType.name, payload: payload)); + SyncWorkerMessage(type: notificationType.name, payload: payload), + ); } Future ping() async { @@ -554,37 +577,41 @@ final class WorkerCommunicationChannel { bool customHttpClient, ) async { final (id, completion) = _newRequest(); - port.postMessage(SyncWorkerMessage( - type: SyncWorkerMessageType.startSynchronization.name, - payload: StartSynchronization( - databaseName: databaseName, - crudThrottleTimeMs: options.crudThrottleTime.inMilliseconds, - retryDelayMs: options.retryDelay.inMilliseconds, - requestId: id, - implementationName: options.source.syncImplementation.name, - schemaJson: jsonEncode(schema), - syncParamsEncoded: switch (options.source.params) { - null => null, - final params => jsonEncode(params), - }, - subscriptions: UpdateSubscriptions(-1, streams), - appMetadataEncoded: switch (options.source.appMetadata) { - null => null, - final appMetadata => jsonEncode(appMetadata), - }, - lockName: await lockName, - customHttpClient: customHttpClient, + port.postMessage( + SyncWorkerMessage( + type: SyncWorkerMessageType.startSynchronization.name, + payload: StartSynchronization( + databaseName: databaseName, + crudThrottleTimeMs: options.crudThrottleTime.inMilliseconds, + retryDelayMs: options.retryDelay.inMilliseconds, + requestId: id, + implementationName: options.source.syncImplementation.name, + schemaJson: jsonEncode(schema), + syncParamsEncoded: switch (options.source.params) { + null => null, + final params => jsonEncode(params), + }, + subscriptions: UpdateSubscriptions(-1, streams), + appMetadataEncoded: switch (options.source.appMetadata) { + null => null, + final appMetadata => jsonEncode(appMetadata), + }, + lockName: await lockName, + customHttpClient: customHttpClient, + ), ), - )); + ); await completion; } Future updateSubscriptions(List streams) async { final (id, completion) = _newRequest(); - port.postMessage(SyncWorkerMessage( - type: SyncWorkerMessageType.updateSubscriptions.name, - payload: UpdateSubscriptions(id, streams), - )); + port.postMessage( + SyncWorkerMessage( + type: SyncWorkerMessageType.updateSubscriptions.name, + payload: UpdateSubscriptions(id, streams), + ), + ); await completion; } diff --git a/packages/powersync/lib/src/web/worker.dart b/packages/powersync/lib/src/web/worker.dart index acf80ec5..57d16cf2 100644 --- a/packages/powersync/lib/src/web/worker.dart +++ b/packages/powersync/lib/src/web/worker.dart @@ -18,8 +18,9 @@ final _isDedicatedWorker = globalContext.has('DedicatedWorkerGlobalScope'); void main() { final controller = PowerSyncAsyncSqliteController(); - final connector = - PowerSyncWorkerConnector((globalContext as Window).location.href); + final connector = PowerSyncWorkerConnector( + (globalContext as Window).location.href, + ); final messagesForDatabaseWorker = StreamController(sync: true); WebSqlite.workerEntrypoint( @@ -37,10 +38,7 @@ void main() { syncWorker.trackPort(message.message as MessagePort); } else { messagesForDatabaseWorker.add( - MessageEvent( - 'message', - MessageEventInit(data: message.message), - ), + MessageEvent('message', MessageEventInit(data: message.message)), ); } } @@ -53,10 +51,10 @@ void main() { EventStreamProviders.connectEvent .forTarget(globalContext as SharedWorkerGlobalScope) .listen((event) { - for (final port in (event as MessageEvent).ports.toDart) { - handlePort(port); - } - }); + for (final port in (event as MessageEvent).ports.toDart) { + handlePort(port); + } + }); } else { EventStreamProviders.messageEvent .forTarget(globalContext as DedicatedWorkerGlobalScope) diff --git a/packages/powersync/lib/src/web/worker_utils.dart b/packages/powersync/lib/src/web/worker_utils.dart index 3c6386ea..371f5c66 100644 --- a/packages/powersync/lib/src/web/worker_utils.dart +++ b/packages/powersync/lib/src/web/worker_utils.dart @@ -8,7 +8,11 @@ import 'package:web/web.dart'; final class PowerSyncAsyncSqliteController extends AsyncSqliteController { @override CommonDatabase openUnderlying( - WasmSqlite3 sqlite3, String path, String vfs, JSAny? additionalData) { + WasmSqlite3 sqlite3, + String path, + String vfs, + JSAny? additionalData, + ) { final options = additionalData == null ? null : additionalData as PowerSyncAdditionalOpenOptions; @@ -56,7 +60,7 @@ final class PowerSyncWorkerConnector implements WorkerConnector { final WorkerConnector _inner; PowerSyncWorkerConnector(String uri) - : _inner = WorkerConnector.defaultWorkers(uri); + : _inner = WorkerConnector.defaultWorkers(uri); @override WorkerHandle? spawnDedicatedWorker() { @@ -82,7 +86,9 @@ final class _SharedWorkerHandle implements WorkerHandle { @override void postMessage(JSAny? msg, JSObject transfer) { _inner.postMessage( - SharedWorkerMessage(isForSyncWorker: false, message: msg), transfer); + SharedWorkerMessage(isForSyncWorker: false, message: msg), + transfer, + ); } @override diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index 300ee8cf..43f23ad3 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Dart and Flutter SDK. Sync Postgres, MongoDB, MySQL or SQL Server with SQLite in your app resolution: workspace environment: - sdk: ^3.6.0 + sdk: ^3.10.0 dependencies: sqlite_async: ^0.14.4 diff --git a/packages/powersync/test/attachments/attachment_test.dart b/packages/powersync/test/attachments/attachment_test.dart index d62b4fdd..3c4b2d9c 100644 --- a/packages/powersync/test/attachments/attachment_test.dart +++ b/packages/powersync/test/attachments/attachment_test.dart @@ -25,7 +25,9 @@ void main() { (rs) => [ for (final row in rs) WatchedAttachmentItem( - id: row['photo_id'] as String, fileExtension: 'jpg') + id: row['photo_id'] as String, + fileExtension: 'jpg', + ), ], ); } @@ -111,7 +113,7 @@ void main() { final nonVerboseMessages = [ for (final LogRecord(:level, :message) in logRecords) - if (level >= Level.INFO) message + if (level >= Level.INFO) message, ]; expect(nonVerboseMessages, [ 'Watching attachments...', @@ -119,7 +121,7 @@ void main() { startsWith('Starting download for attachment'), startsWith('Successfully downloaded file'), 'Deleting 1 archived attachments (exceeding maxArchivedCount=0)...', - 'Deleted 1 archived attachments.' + 'Deleted 1 archived attachments.', ]); }); @@ -134,11 +136,15 @@ void main() { // Wait for attachment to sync. await expectLater( - attachments, - emitsThrough([ - isA() - .having((e) => e.state, 'state', AttachmentState.synced) - ])); + attachments, + emitsThrough([ + isA().having( + (e) => e.state, + 'state', + AttachmentState.synced, + ), + ]), + ); expect(await localStorage.fileExists('picture_id.jpg'), isTrue); }); @@ -156,7 +162,7 @@ void main() { 1, AttachmentState.synced.toInt(), 1, - "" + "", ], ); await attachments.next; @@ -229,8 +235,11 @@ void main() { await expectLater( attachments, emitsThrough([ - isA() - .having((e) => e.state, 'state', AttachmentState.synced) + isA().having( + (e) => e.state, + 'state', + AttachmentState.synced, + ), ]), ); @@ -268,12 +277,16 @@ void main() { await expectLater( attachments, emitsThrough([ - isA() - .having((e) => e.state, 'state', AttachmentState.synced) + isA().having( + (e) => e.state, + 'state', + AttachmentState.synced, + ), ]), ); - final [id as String, localUri as String] = - (await db.get('SELECT id, local_uri FROM attachments_queue')).values; + final [id as String, localUri as String] = (await db.get( + 'SELECT id, local_uri FROM attachments_queue', + )).values; verify(remoteStorage.downloadFile(argThat(isAttachment(id)))); expect(await localStorage.fileExists(localUri), isTrue); @@ -282,8 +295,11 @@ void main() { await expectLater( attachments, emitsThrough([ - isA() - .having((e) => e.state, 'state', AttachmentState.archived) + isA().having( + (e) => e.state, + 'state', + AttachmentState.archived, + ), ]), ); @@ -292,8 +308,11 @@ void main() { await expectLater( attachments, emitsThrough([ - isA() - .having((e) => e.state, 'state', AttachmentState.synced) + isA().having( + (e) => e.state, + 'state', + AttachmentState.synced, + ), ]), ); expect(await localStorage.fileExists(localUri), isTrue); @@ -304,7 +323,10 @@ void main() { test('skip failed download', () async { Future errorHandler( - Attachment attachment, Object exception, StackTrace trace) async { + Attachment attachment, + Object exception, + StackTrace trace, + ) async { return false; } @@ -335,12 +357,18 @@ void main() { ); expect(await attachments.next, [ - isA() - .having((e) => e.state, 'state', AttachmentState.queuedDownload) + isA().having( + (e) => e.state, + 'state', + AttachmentState.queuedDownload, + ), ]); expect(await attachments.next, [ - isA() - .having((e) => e.state, 'state', AttachmentState.archived) + isA().having( + (e) => e.state, + 'state', + AttachmentState.archived, + ), ]); expect( @@ -370,10 +398,12 @@ void main() { ); final queuedDownloadCounts = StreamQueue( - db.watchUnthrottled( - 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', - parameters: [AttachmentState.queuedDownload.index], - ).map((rs) => rs[0]['c'] as int), + db + .watchUnthrottled( + 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', + parameters: [AttachmentState.queuedDownload.index], + ) + .map((rs) => rs[0]['c'] as int), ); await queue.startSync(); @@ -398,8 +428,7 @@ void main() { await queuedDownloadCounts.cancel(); }); - test('attachments_queue commits per-attachment, not at end of batch', - () async { + test('attachments_queue commits per-attachment, not at end of batch', () async { _stubSlowRemoteStorage(remoteStorage, const Duration(milliseconds: 100)); queue = AttachmentQueue( db: db, @@ -411,10 +440,12 @@ void main() { ); final syncedCounts = StreamQueue( - db.watchUnthrottled( - 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', - parameters: [AttachmentState.synced.index], - ).map((rs) => rs[0]['c'] as int), + db + .watchUnthrottled( + 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', + parameters: [AttachmentState.synced.index], + ) + .map((rs) => rs[0]['c'] as int), ); await queue.startSync(); @@ -448,10 +479,12 @@ void main() { ); final queuedDownloadCounts = StreamQueue( - db.watchUnthrottled( - 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', - parameters: [AttachmentState.queuedDownload.index], - ).map((rs) => rs[0]['c'] as int), + db + .watchUnthrottled( + 'SELECT COUNT(*) AS c FROM attachments_queue WHERE state = ?', + parameters: [AttachmentState.queuedDownload.index], + ) + .map((rs) => rs[0]['c'] as int), ); await queue.startSync(); @@ -489,8 +522,9 @@ void main() { extension on PowerSyncDatabase { Stream> get attachments { - return watch('SELECT * FROM attachments_queue') - .map((rs) => rs.map(Attachment.fromRow).toList()); + return watch( + 'SELECT * FROM attachments_queue', + ).map((rs) => rs.map(Attachment.fromRow).toList()); } } @@ -505,7 +539,9 @@ final class MockRemoteStorage extends Mock implements RemoteStorage { @override Future uploadFile( - Stream? fileData, Attachment? attachment) async { + Stream? fileData, + Attachment? attachment, + ) async { await noSuchMethod(Invocation.method(#uploadFile, [fileData, attachment])); } @@ -523,8 +559,11 @@ final class MockRemoteStorage extends Mock implements RemoteStorage { } final _schema = Schema([ - Table('users', - [Column.text('name'), Column.text('email'), Column.text('photo_id')]), + Table('users', [ + Column.text('name'), + Column.text('email'), + Column.text('photo_id'), + ]), AttachmentsQueueTable(), ]); diff --git a/packages/powersync/test/attachments/local_storage_test.dart b/packages/powersync/test/attachments/local_storage_test.dart index be7da8d4..26413db5 100644 --- a/packages/powersync/test/attachments/local_storage_test.dart +++ b/packages/powersync/test/attachments/local_storage_test.dart @@ -110,21 +110,23 @@ void main() { }); group('edge cases and robustness', () { - test('saveFile with empty data writes empty file and returns 0 size', - () async { - const filePath = 'empty_file'; + test( + 'saveFile with empty data writes empty file and returns 0 size', + () async { + const filePath = 'empty_file'; - final size = await storage.saveFile(filePath, Stream.empty()); - expect(size, 0); + final size = await storage.saveFile(filePath, Stream.empty()); + expect(size, 0); - final resultStream = storage.readFile(filePath); - final chunks = await resultStream.toList(); - expect(chunks, isEmpty); + final resultStream = storage.readFile(filePath); + final chunks = await resultStream.toList(); + expect(chunks, isEmpty); - final file = File(p.join(d.sandbox, filePath)); - expect(await file.exists(), isTrue); - expect(await file.length(), 0); - }); + final file = File(p.join(d.sandbox, filePath)); + expect(await file.exists(), isTrue); + expect(await file.length(), 0); + }, + ); test('readFile preserves byte order (chunking may differ)', () async { const filePath = 'ordered_chunks'; @@ -133,8 +135,9 @@ void main() { Uint8List.fromList([3, 4]), Uint8List.fromList([5, 6, 7, 8]), ]; - final expectedBytes = - Uint8List.fromList(chunks.expand((c) => c).toList()); + final expectedBytes = Uint8List.fromList( + chunks.expand((c) => c).toList(), + ); await storage.saveFile(filePath, Stream.value(expectedBytes)); final outChunks = await storage.readFile(filePath).toList(); @@ -165,20 +168,22 @@ void main() { expect(await storage.fileExists(filePath), isTrue); }); - test('clear works even if base directory was removed externally', - () async { - await storage.initialize(); + test( + 'clear works even if base directory was removed externally', + () async { + await storage.initialize(); - // Remove the base dir manually - final baseDir = Directory(d.sandbox); - if (await baseDir.exists()) { - await baseDir.delete(recursive: true); - } + // Remove the base dir manually + final baseDir = Directory(d.sandbox); + if (await baseDir.exists()) { + await baseDir.delete(recursive: true); + } - // Calling clear should recreate base dir - await storage.clear(); - expect(await baseDir.exists(), isTrue); - }); + // Calling clear should recreate base dir + await storage.clear(); + expect(await baseDir.exists(), isTrue); + }, + ); test('supports unicode and emoji filenames', () async { const filePath = '測試_файл_📷.bin'; @@ -191,16 +196,19 @@ void main() { await d.file(filePath, bytes).validate(); }); - test('readFile accepts mediaType parameter (ignored by IO impl)', - () async { - const filePath = 'with_media_type'; - final data = Uint8List.fromList([1, 2, 3]); - await storage.saveFile(filePath, Stream.value(data)); - - final result = - await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); - expect(result, equals([data])); - }); + test( + 'readFile accepts mediaType parameter (ignored by IO impl)', + () async { + const filePath = 'with_media_type'; + final data = Uint8List.fromList([1, 2, 3]); + await storage.saveFile(filePath, Stream.value(data)); + + final result = await storage + .readFile(filePath, mediaType: 'image/jpeg') + .toList(); + expect(result, equals([data])); + }, + ); }); group('deleteFile', () { @@ -227,8 +235,9 @@ void main() { group('initialize and clear', () { test('initialize creates the base directory', () async { - final newStorage = - IOLocalStorage(Directory(p.join(d.sandbox, 'new_dir'))); + final newStorage = IOLocalStorage( + Directory(p.join(d.sandbox, 'new_dir')), + ); final baseDir = Directory(p.join(d.sandbox, 'new_dir')); expect(await baseDir.exists(), isFalse); diff --git a/packages/powersync/test/connected_test.dart b/packages/powersync/test/connected_test.dart index 008bce21..c210a4a2 100644 --- a/packages/powersync/test/connected_test.dart +++ b/packages/powersync/test/connected_test.dart @@ -36,15 +36,17 @@ void main() { final testServer = await createTestServer(); final connector = TestConnector(() async { return PowerSyncCredentials( - endpoint: testServer.uri.toString(), - token: 'token not used here', - expiresAt: DateTime.now()); + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now(), + ); }); final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: defaultSchema, - maxReaders: 3); + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3, + ); addTearDown(() => {db.close()}); await db.initialize(); @@ -70,21 +72,26 @@ void main() { int uploadCounter = 0; var uploadTriggeredCompleter = Completer(); final testServer = await createTestServer(); - final connector = TestConnector(() async { - return PowerSyncCredentials( + final connector = TestConnector( + () async { + return PowerSyncCredentials( endpoint: testServer.uri.toString(), token: 'token not used here', - expiresAt: DateTime.now()); - }, uploadData: (database) async { - uploadCounter++; - uploadTriggeredCompleter.complete(); - throw Exception('No uploads occur here'); - }); + expiresAt: DateTime.now(), + ); + }, + uploadData: (database) async { + uploadCounter++; + uploadTriggeredCompleter.complete(); + throw Exception('No uploads occur here'); + }, + ); final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: defaultSchema, - maxReaders: 3); + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3, + ); // Shorter retry delay, to speed up tests // ignore: deprecated_member_use_from_same_package db.retryDelay = Duration(milliseconds: 10); @@ -92,8 +99,9 @@ void main() { await db.initialize(); // Create an item which should trigger an upload. - await db.execute( - 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); + await db.execute('INSERT INTO customers (id, name) VALUES (uuid(), ?)', [ + 'steven', + ]); // Create a new completer to await the next upload uploadTriggeredCompleter = Completer(); @@ -112,8 +120,13 @@ void main() { // Connection attempt should initially fail await expectLater( db.statusStream, - emitsThrough(isA() - .having((e) => e.downloadError, 'downloadError', isNotNull)), + emitsThrough( + isA().having( + (e) => e.downloadError, + 'downloadError', + isNotNull, + ), + ), ); // Now send a valid command. Which will result in successful connection @@ -122,7 +135,8 @@ void main() { await expectLater( db.statusStream, emitsThrough( - isA().having((e) => e.connected, 'connected', isTrue)), + isA().having((e) => e.connected, 'connected', isTrue), + ), ); await uploadTriggeredCompleter.future; @@ -131,87 +145,106 @@ void main() { await db.disconnect(); }); - test('should persist local changes when there is no write checkpoint', - () async { - final testServer = await createTestServer(); - final connector = TestConnector(() async { - return PowerSyncCredentials( - endpoint: testServer.uri.toString(), - token: 'token not used here', - expiresAt: DateTime.now()); - }, uploadData: (database) async { - final tx = await database.getNextCrudTransaction(); - if (tx != null) { - await tx.complete(); - } - }); - - final db = PowerSyncDatabase.withFactory( + test( + 'should persist local changes when there is no write checkpoint', + () async { + final testServer = await createTestServer(); + final connector = TestConnector( + () async { + return PowerSyncCredentials( + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now(), + ); + }, + uploadData: (database) async { + final tx = await database.getNextCrudTransaction(); + if (tx != null) { + await tx.complete(); + } + }, + ); + + final db = PowerSyncDatabase.withFactory( await testUtils.testFactory(path: path), schema: defaultSchema, - maxReaders: 3); - addTearDown(() => {db.close()}); - await db.initialize(); - - // Create an item which should trigger an upload. - await db.execute( - 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); - - // Manually simulate upload before connecting. - // This is simpler than doing this via connect() and waiting for it to complete. - await connector.uploadData(db); - - // Check that the data is present locally - expect( + maxReaders: 3, + ); + addTearDown(() => {db.close()}); + await db.initialize(); + + // Create an item which should trigger an upload. + await db.execute( + 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', + ['steven'], + ); + + // Manually simulate upload before connecting. + // This is simpler than doing this via connect() and waiting for it to complete. + await connector.uploadData(db); + + // Check that the data is present locally + expect( await db.getAll('select name from customers'), equals([ - {'name': 'steven'} - ])); - - // Connect and send a checkpoint back, but no write checkpoint. - testServer - .addEvent('{"checkpoint": {"last_op_id": "10", "buckets": []}}\n'); - testServer.addEvent('{"checkpoint_complete": {"last_op_id": "10"}}\n'); - - // Now connect and wait for sync to complete - await db.connect(connector: connector); - await db.statusStream - .firstWhere((status) => status.connected && status.downloading); - await Future.delayed(Duration(milliseconds: 20)); - expect( + {'name': 'steven'}, + ]), + ); + + // Connect and send a checkpoint back, but no write checkpoint. + testServer.addEvent( + '{"checkpoint": {"last_op_id": "10", "buckets": []}}\n', + ); + testServer.addEvent('{"checkpoint_complete": {"last_op_id": "10"}}\n'); + + // Now connect and wait for sync to complete + await db.connect(connector: connector); + await db.statusStream.firstWhere( + (status) => status.connected && status.downloading, + ); + await Future.delayed(Duration(milliseconds: 20)); + expect( await db.getAll('select name from customers'), equals([ - {'name': 'steven'} - ])); - }); + {'name': 'steven'}, + ]), + ); + }, + ); test('should remove local changes when there a write checkpoint', () async { // The only difference between this and the one above, is that the synced // checkpoint here contains a write checkpoint, matching the write-checkpoint2.json // API. This will trigger the local changes to be removed. final testServer = await createTestServer(); - final connector = TestConnector(() async { - return PowerSyncCredentials( + final connector = TestConnector( + () async { + return PowerSyncCredentials( endpoint: testServer.uri.toString(), token: 'token not used here', - expiresAt: DateTime.now()); - }, uploadData: (database) async { - final tx = await database.getNextCrudTransaction(); - if (tx != null) { - await tx.complete(); - } - }); + expiresAt: DateTime.now(), + ); + }, + uploadData: (database) async { + final tx = await database.getNextCrudTransaction(); + if (tx != null) { + await tx.complete(); + } + }, + ); final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: defaultSchema, - maxReaders: 3); + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3, + ); addTearDown(() => {db.close()}); await db.initialize(); // Create an item which should trigger an upload. - await db.execute( - 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); + await db.execute('INSERT INTO customers (id, name) VALUES (uuid(), ?)', [ + 'steven', + ]); // Manually simulate upload before connecting. // This is simpler than doing this via connect() and waiting for it to complete. @@ -219,20 +252,23 @@ void main() { // Check that the data is present locally expect( - await db.getAll('select name from customers'), - equals([ - {'name': 'steven'} - ])); + await db.getAll('select name from customers'), + equals([ + {'name': 'steven'}, + ]), + ); // Connect and send a checkpoint back, but no write checkpoint. testServer.addEvent( - '{"checkpoint": {"last_op_id": "10", "buckets": [], "write_checkpoint": "10"}}\n'); + '{"checkpoint": {"last_op_id": "10", "buckets": [], "write_checkpoint": "10"}}\n', + ); testServer.addEvent('{"checkpoint_complete": {"last_op_id": "10"}}\n'); // Now connect and wait for sync to complete await db.connect(connector: connector); - await db.statusStream - .firstWhere((status) => status.connected && status.downloading); + await db.statusStream.firstWhere( + (status) => status.connected && status.downloading, + ); await Future.delayed(Duration(milliseconds: 20)); expect(await db.getAll('select name from customers'), equals([])); }); diff --git a/packages/powersync/test/credentials_test.dart b/packages/powersync/test/credentials_test.dart index fcdb2551..a3d9fbb7 100644 --- a/packages/powersync/test/credentials_test.dart +++ b/packages/powersync/test/credentials_test.dart @@ -7,8 +7,10 @@ void main() { // Specifically test a token with a "-" character and missing padding final token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ9Pn0-YWIiLCJpYXQiOjE3MDc3Mzk0MDAsImV4cCI6MTcwNzczOTUwMH0=.IVoAtpJ7jfwLbqlyJGYHPCvljLis_fHj2Qvdqlj8AQU'; - expect(PowerSyncCredentials.getExpiryDate(token)?.toUtc(), - equals(DateTime.parse('2024-02-12T12:05:00Z'))); + expect( + PowerSyncCredentials.getExpiryDate(token)?.toUtc(), + equals(DateTime.parse('2024-02-12T12:05:00Z')), + ); }); }); } diff --git a/packages/powersync/test/crud_test.dart b/packages/powersync/test/crud_test.dart index 50c201a4..1fb99dc1 100644 --- a/packages/powersync/test/crud_test.dart +++ b/packages/powersync/test/crud_test.dart @@ -26,251 +26,318 @@ void main() { test('INSERT', () async { expect(await powersync.getAll('SELECT * FROM ps_crud'), equals([])); await powersync.execute( - 'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']); + 'INSERT INTO assets(id, description) VALUES(?, ?)', + [testId, 'test'], + ); expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - { - 'data': - '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test"}}' - } - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + { + 'data': + '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test"}}', + }, + ]), + ); var tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId, equals(1)); expect( - tx.crud, - equals([ - CrudEntry( - 1, UpdateType.put, 'assets', testId, 1, {"description": "test"}) - ])); + tx.crud, + equals([ + CrudEntry(1, UpdateType.put, 'assets', testId, 1, { + "description": "test", + }), + ]), + ); }); test('INSERT OR REPLACE', () async { await powersync.execute( - 'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']); + 'INSERT INTO assets(id, description) VALUES(?, ?)', + [testId, 'test'], + ); await powersync.execute('DELETE FROM ps_crud WHERE 1'); // Replace await powersync.execute( - 'INSERT OR REPLACE INTO assets(id, description) VALUES(?, ?)', - [testId, 'test2']); + 'INSERT OR REPLACE INTO assets(id, description) VALUES(?, ?)', + [testId, 'test2'], + ); // This generates another PUT expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - { - 'data': - '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test2"}}' - } - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + { + 'data': + '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test2"}}', + }, + ]), + ); - expect(await powersync.get('SELECT count(*) AS count FROM assets'), - equals({'count': 1})); + expect( + await powersync.get('SELECT count(*) AS count FROM assets'), + equals({'count': 1}), + ); // Make sure uniqueness is enforced - expect(() async { - await powersync.execute( + expect( + () async { + await powersync.execute( 'INSERT INTO assets(id, description) VALUES(?, ?)', - [testId, 'test3']); - }, - throwsA((dynamic e) => + [testId, 'test3'], + ); + }, + throwsA( + (dynamic e) => e is SqliteException && - e.message.contains('UNIQUE constraint failed'))); + e.message.contains('UNIQUE constraint failed'), + ), + ); }); test('UPDATE', () async { await powersync.execute( - 'INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', - [testId, 'test', 'test']); + 'INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', + [testId, 'test', 'test'], + ); await powersync.execute('DELETE FROM ps_crud WHERE 1'); await powersync.execute( - 'UPDATE assets SET description = ? WHERE id = ?', ['test2', testId]); + 'UPDATE assets SET description = ? WHERE id = ?', + ['test2', testId], + ); expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - { - 'data': - '{"op":"PATCH","id":"$testId","type":"assets","data":{"description":"test2"}}' - } - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + { + 'data': + '{"op":"PATCH","id":"$testId","type":"assets","data":{"description":"test2"}}', + }, + ]), + ); var tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId, equals(2)); expect( - tx.crud, - equals([ - CrudEntry(2, UpdateType.patch, 'assets', testId, 2, - {"description": "test2"}) - ])); + tx.crud, + equals([ + CrudEntry(2, UpdateType.patch, 'assets', testId, 2, { + "description": "test2", + }), + ]), + ); }); test('DELETE', () async { await powersync.execute( - 'INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', - [testId, 'test', 'test']); + 'INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', + [testId, 'test', 'test'], + ); await powersync.execute('DELETE FROM ps_crud WHERE 1'); await powersync.execute('DELETE FROM assets WHERE id = ?', [testId]); expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - {'data': '{"op":"DELETE","id":"$testId","type":"assets"}'} - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + {'data': '{"op":"DELETE","id":"$testId","type":"assets"}'}, + ]), + ); var tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId, equals(2)); - expect(tx.crud, - equals([CrudEntry(2, UpdateType.delete, 'assets', testId, 2, null)])); + expect( + tx.crud, + equals([CrudEntry(2, UpdateType.delete, 'assets', testId, 2, null)]), + ); }); test('UPSERT not supported', () async { // Just shows that we cannot currently do this - expect(() async { - await powersync.execute( + expect( + () async { + await powersync.execute( 'INSERT INTO assets(id, description) VALUES(?, ?) ON CONFLICT DO UPDATE SET description = ?', - [testId, 'test2', 'test3']); - }, - throwsA((dynamic e) => + [testId, 'test2', 'test3'], + ); + }, + throwsA( + (dynamic e) => e is SqliteException && - e.message.contains('cannot UPSERT a view'))); + e.message.contains('cannot UPSERT a view'), + ), + ); }); test('INSERT-only tables', () async { await powersync.disconnectAndClear(); await powersync.close(); powersync = await testUtils.setupPowerSync( - path: path, - schema: const Schema([ - Table.insertOnly( - 'logs', [Column.text('level'), Column.text('content')]) - ])); + path: path, + schema: const Schema([ + Table.insertOnly('logs', [ + Column.text('level'), + Column.text('content'), + ]), + ]), + ); expect(await powersync.getAll('SELECT * FROM ps_crud'), equals([])); await powersync.execute( - 'INSERT INTO logs(id, level, content) VALUES(?, ?, ?)', - [testId, 'INFO', 'test log']); + 'INSERT INTO logs(id, level, content) VALUES(?, ?, ?)', + [testId, 'INFO', 'test log'], + ); expect( - await powersync.getAll( - "SELECT json_extract(data, '\$.id') as id FROM ps_crud ORDER BY id"), - equals([ - {'id': testId} - ])); + await powersync.getAll( + "SELECT json_extract(data, '\$.id') as id FROM ps_crud ORDER BY id", + ), + equals([ + {'id': testId}, + ]), + ); expect(await powersync.getAll('SELECT * FROM logs'), equals([])); var tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId, equals(1)); expect( - tx.crud, - equals([ - CrudEntry(1, UpdateType.put, 'logs', testId, 1, - {"level": "INFO", "content": "test log"}) - ])); + tx.crud, + equals([ + CrudEntry(1, UpdateType.put, 'logs', testId, 1, { + "level": "INFO", + "content": "test log", + }), + ]), + ); }); test('big numbers - integer', () async { const bigNumber = 1 << 62; - await powersync.execute( - 'INSERT INTO assets(id, quantity) VALUES(?, ?)', [testId, bigNumber]); + await powersync.execute('INSERT INTO assets(id, quantity) VALUES(?, ?)', [ + testId, + bigNumber, + ]); expect( - await powersync - .get('SELECT quantity FROM assets WHERE id = ?', [testId]), - equals({'quantity': bigNumber})); + await powersync.get('SELECT quantity FROM assets WHERE id = ?', [ + testId, + ]), + equals({'quantity': bigNumber}), + ); expect( - await powersync.getAll( - "SELECT json_extract(data, '\$.id') as id FROM ps_crud ORDER BY id"), - equals([ - {"id": testId} - ])); + await powersync.getAll( + "SELECT json_extract(data, '\$.id') as id FROM ps_crud ORDER BY id", + ), + equals([ + {"id": testId}, + ]), + ); var tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId, equals(1)); expect( - tx.crud, - equals([ - CrudEntry( - 1, UpdateType.put, 'assets', testId, 1, {"quantity": bigNumber}) - ])); + tx.crud, + equals([ + CrudEntry(1, UpdateType.put, 'assets', testId, 1, { + "quantity": bigNumber, + }), + ]), + ); }); test('big numbers - text', () async { const bigNumber = 1 << 62; - await powersync.execute('INSERT INTO assets(id, quantity) VALUES(?, ?)', - [testId, '$bigNumber']); + await powersync.execute('INSERT INTO assets(id, quantity) VALUES(?, ?)', [ + testId, + '$bigNumber', + ]); // Cast as INTEGER when querying expect( - await powersync - .get('SELECT quantity FROM assets WHERE id = ?', [testId]), - equals({'quantity': bigNumber})); + await powersync.get('SELECT quantity FROM assets WHERE id = ?', [ + testId, + ]), + equals({'quantity': bigNumber}), + ); // Not cast as part of crud / persistance expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - { - 'data': - '{"op":"PUT","id":"$testId","type":"assets","data":{"quantity":"$bigNumber"}}' - } - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + { + 'data': + '{"op":"PUT","id":"$testId","type":"assets","data":{"quantity":"$bigNumber"}}', + }, + ]), + ); await powersync.execute('DELETE FROM ps_crud WHERE 1'); await powersync.execute( - 'UPDATE assets SET quantity = quantity + 1 WHERE id = ?', [testId]); + 'UPDATE assets SET quantity = quantity + 1 WHERE id = ?', + [testId], + ); expect( - await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), - equals([ - { - 'data': - '{"op":"PATCH","id":"$testId","type":"assets","data":{"quantity":${bigNumber + 1}}}' - } - ])); + await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([ + { + 'data': + '{"op":"PATCH","id":"$testId","type":"assets","data":{"quantity":${bigNumber + 1}}}', + }, + ]), + ); }); test('Transaction grouping', () async { expect(await powersync.getAll('SELECT * FROM ps_crud'), equals([])); await powersync.writeTransaction((tx) async { - await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', - [testId, 'test1']); - await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', - ['test2', 'test2']); + await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [ + testId, + 'test1', + ]); + await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [ + 'test2', + 'test2', + ]); }); await powersync.writeTransaction((tx) async { - await tx.execute('UPDATE assets SET description = ? WHERE id = ?', - ['updated', testId]); + await tx.execute('UPDATE assets SET description = ? WHERE id = ?', [ + 'updated', + testId, + ]); }); var tx1 = (await powersync.getNextCrudTransaction())!; expect(tx1.transactionId, equals(1)); expect( - tx1.crud, - equals([ - CrudEntry(1, UpdateType.put, 'assets', testId, 1, - {"description": "test1"}), - CrudEntry(2, UpdateType.put, 'assets', 'test2', 1, - {"description": "test2"}) - ])); + tx1.crud, + equals([ + CrudEntry(1, UpdateType.put, 'assets', testId, 1, { + "description": "test1", + }), + CrudEntry(2, UpdateType.put, 'assets', 'test2', 1, { + "description": "test2", + }), + ]), + ); await tx1.complete(); var tx2 = (await powersync.getNextCrudTransaction())!; expect(tx2.transactionId, equals(2)); expect( - tx2.crud, - equals([ - CrudEntry(3, UpdateType.patch, 'assets', testId, 2, - {"description": "updated"}), - ])); + tx2.crud, + equals([ + CrudEntry(3, UpdateType.patch, 'assets', testId, 2, { + "description": "updated", + }), + ]), + ); await tx2.complete(); expect(await powersync.getNextCrudTransaction(), equals(null)); }); @@ -309,76 +376,89 @@ void main() { }); test('include metadata', () async { - await powersync.updateSchema(Schema([ - Table( - 'lists', - [Column.text('name')], - trackMetadata: true, - ) - ])); + await powersync.updateSchema( + Schema([ + Table('lists', [Column.text('name')], trackMetadata: true), + ]), + ); await powersync.execute( - 'INSERT INTO lists (id, name, _metadata) VALUES (uuid(), ?, ?)', - ['entry', 'so meta']); + 'INSERT INTO lists (id, name, _metadata) VALUES (uuid(), ?, ?)', + ['entry', 'so meta'], + ); final batch = await powersync.getNextCrudTransaction(); expect(batch!.crud[0].metadata, 'so meta'); }); test('include old values', () async { - await powersync.updateSchema(Schema([ - Table( - 'lists', - [Column.text('name'), Column.text('content')], - trackPreviousValues: TrackPreviousValuesOptions(), - ) - ])); + await powersync.updateSchema( + Schema([ + Table('lists', [ + Column.text('name'), + Column.text('content'), + ], trackPreviousValues: TrackPreviousValuesOptions()), + ]), + ); await powersync.execute( - 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', - ['entry', 'content']); + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['entry', 'content'], + ); await powersync.execute('DELETE FROM ps_crud;'); await powersync.execute('UPDATE lists SET name = ?;', ['new name']); final batch = await powersync.getNextCrudTransaction(); - expect(batch!.crud[0].previousValues, - {'name': 'entry', 'content': 'content'}); + expect(batch!.crud[0].previousValues, { + 'name': 'entry', + 'content': 'content', + }); }); test('include old values with column filter', () async { - await powersync.updateSchema(Schema([ - Table( - 'lists', - [Column.text('name'), Column.text('content')], - trackPreviousValues: - TrackPreviousValuesOptions(columnFilter: ['name']), - ) - ])); + await powersync.updateSchema( + Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + trackPreviousValues: TrackPreviousValuesOptions( + columnFilter: ['name'], + ), + ), + ]), + ); await powersync.execute( - 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', - ['name', 'content']); + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content'], + ); await powersync.execute('DELETE FROM ps_crud;'); - await powersync.execute('UPDATE lists SET name = ?, content = ?', - ['new name', 'new content']); + await powersync.execute('UPDATE lists SET name = ?, content = ?', [ + 'new name', + 'new content', + ]); final batch = await powersync.getNextCrudTransaction(); expect(batch!.crud[0].previousValues, {'name': 'name'}); }); test('include old values when changed', () async { - await powersync.updateSchema(Schema([ - Table( - 'lists', - [Column.text('name'), Column.text('content')], - trackPreviousValues: - TrackPreviousValuesOptions(onlyWhenChanged: true), - ) - ])); + await powersync.updateSchema( + Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + trackPreviousValues: TrackPreviousValuesOptions( + onlyWhenChanged: true, + ), + ), + ]), + ); await powersync.execute( - 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', - ['name', 'content']); + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content'], + ); await powersync.execute('DELETE FROM ps_crud;'); await powersync.execute('UPDATE lists SET name = ?', ['new name']); @@ -387,16 +467,16 @@ void main() { }); test('ignore empty update', () async { - await powersync.updateSchema(Schema([ - Table( - 'lists', - [Column.text('name')], - ignoreEmptyUpdates: true, - ) - ])); - - await powersync - .execute('INSERT INTO lists (id, name) VALUES (uuid(), ?)', ['name']); + await powersync.updateSchema( + Schema([ + Table('lists', [Column.text('name')], ignoreEmptyUpdates: true), + ]), + ); + + await powersync.execute( + 'INSERT INTO lists (id, name) VALUES (uuid(), ?)', + ['name'], + ); await powersync.execute('DELETE FROM ps_crud;'); await powersync.execute('UPDATE lists SET name = ?;', ['name']); expect(await powersync.getNextCrudTransaction(), isNull); @@ -409,8 +489,10 @@ void main() { 'SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)', [json.encode(table), 'users_insert', 'INSERT'], ); - await powersync.execute( - 'INSERT INTO users (id, name) VALUES (?, ?);', ['id', 'user']); + await powersync.execute('INSERT INTO users (id, name) VALUES (?, ?);', [ + 'id', + 'user', + ]); final tx = await powersync.getNextCrudTransaction(); expect(tx!.crud, [ @@ -435,31 +517,39 @@ void main() { ), ); - await powersync - .execute('CREATE TABLE users (id TEXT, name TEXT, local TEXT);'); await powersync.execute( - 'INSERT INTO users (id, name, local) VALUES (?, ?, ?);', - ['id', 'name', 'local']); + 'CREATE TABLE users (id TEXT, name TEXT, local TEXT);', + ); + await powersync.execute( + 'INSERT INTO users (id, name, local) VALUES (?, ?, ?);', + ['id', 'name', 'local'], + ); await powersync.execute( 'SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)', [json.encode(table), 'users_update', 'UPDATE'], ); - await powersync.execute('UPDATE users SET name = ?, local = ?', - ['updated_name', 'updated_local']); + await powersync.execute('UPDATE users SET name = ?, local = ?', [ + 'updated_name', + 'updated_local', + ]); // This should not generate a CRUD entry because the only syned column is // not affected. - await powersync.execute('UPDATE users SET name = ?, local = ?', - ['updated_name', 'updated_local_2']); + await powersync.execute('UPDATE users SET name = ?, local = ?', [ + 'updated_name', + 'updated_local_2', + ]); final tx = await powersync.getNextCrudTransaction(); expect(tx!.crud, [ isA() .having((e) => e.op, 'op', UpdateType.patch) .having((e) => e.id, 'id', 'id') - .having((e) => e.opData, 'opData', {'name': 'updated_name'}).having( - (e) => e.previousValues, 'previousValues', {'name': 'name'}), + .having((e) => e.opData, 'opData', {'name': 'updated_name'}) + .having((e) => e.previousValues, 'previousValues', { + 'name': 'name', + }), ]); }); }); diff --git a/packages/powersync/test/database/core_version_test.dart b/packages/powersync/test/database/core_version_test.dart index 0d08d2c8..07b7e49d 100644 --- a/packages/powersync/test/database/core_version_test.dart +++ b/packages/powersync/test/database/core_version_test.dart @@ -26,14 +26,20 @@ void main() { }); test('checkSupported', () { - expect(PowerSyncCoreVersion.parse('0.3.10').checkSupported, - throwsA(isA())); - expect(PowerSyncCoreVersion.parse('1.0.0').checkSupported, - throwsA(isA())); + expect( + PowerSyncCoreVersion.parse('0.3.10').checkSupported, + throwsA(isA()), + ); + expect( + PowerSyncCoreVersion.parse('1.0.0').checkSupported, + throwsA(isA()), + ); PowerSyncCoreVersion.minimum.checkSupported(); - expect(PowerSyncCoreVersion.maximumExclusive.checkSupported, - throwsA(isA())); + expect( + PowerSyncCoreVersion.maximumExclusive.checkSupported, + throwsA(isA()), + ); }); }); } diff --git a/packages/powersync/test/database/encryption_test.dart b/packages/powersync/test/database/encryption_test.dart index 1b759054..f73645c4 100644 --- a/packages/powersync/test/database/encryption_test.dart +++ b/packages/powersync/test/database/encryption_test.dart @@ -17,39 +17,42 @@ void main() { test('generates pragma statements', () { expect( - EncryptionOptions(key: 'foo', sqlcipherCompatibility: false) - .pragmaStatements(), + EncryptionOptions( + key: 'foo', + sqlcipherCompatibility: false, + ).pragmaStatements(), ["PRAGMA key = 'foo'"], ); expect( - EncryptionOptions(key: 'foo', sqlcipherCompatibility: true) - .pragmaStatements(), + EncryptionOptions( + key: 'foo', + sqlcipherCompatibility: true, + ).pragmaStatements(), [ "PRAGMA cipher = 'sqlcipher'", 'PRAGMA legacy = 4', - "PRAGMA key = 'foo'" + "PRAGMA key = 'foo'", ], ); expect( - EncryptionOptions(key: "f'o'o", sqlcipherCompatibility: false) - .pragmaStatements(), + EncryptionOptions( + key: "f'o'o", + sqlcipherCompatibility: false, + ).pragmaStatements(), ["PRAGMA key = 'f''o''o'"], ); }); - group( - 'without encryption', - () { - test('throws when encryption options are used', () async { - await expectLater(() async { - await testUtils.setupPowerSync( - encryption: EncryptionOptions(key: 'foo')); - }, throwsA(anything)); - }); - }, - tags: 'require_no_encryption', - ); + group('without encryption', () { + test('throws when encryption options are used', () async { + await expectLater(() async { + await testUtils.setupPowerSync( + encryption: EncryptionOptions(key: 'foo'), + ); + }, throwsA(anything)); + }); + }, tags: 'require_no_encryption'); // To run the following tests, uncomment hook options in the monorepo's // pubspec.yaml and run dart test -P encryption. @@ -65,8 +68,10 @@ void main() { encryption: EncryptionOptions(key: 'foo'), ); - await db.execute('INSERT INTO customers (id, name) VALUES (uuid(), ?)', - ['secret customer']); + await db.execute( + 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', + ['secret customer'], + ); await db.close(); } diff --git a/packages/powersync/test/devtools/app.dart b/packages/powersync/test/devtools/app.dart index 54834790..f3afab5b 100644 --- a/packages/powersync/test/devtools/app.dart +++ b/packages/powersync/test/devtools/app.dart @@ -17,7 +17,7 @@ void main(List args) async { } const schema = Schema([ - Table('users', [Column.text('name')]) + Table('users', [Column.text('name')]), ]); final database = PowerSyncDatabase(schema: schema, path: databasePath); await database.initialize(); diff --git a/packages/powersync/test/devtools/devtools_test.dart b/packages/powersync/test/devtools/devtools_test.dart index be876311..c96060e7 100644 --- a/packages/powersync/test/devtools/devtools_test.dart +++ b/packages/powersync/test/devtools/devtools_test.dart @@ -68,8 +68,10 @@ void main() { }); test('can get version', () async { - final response = await vm.callServiceExtension('ext.powersync.version', - isolateId: isolateId); + final response = await vm.callServiceExtension( + 'ext.powersync.version', + isolateId: isolateId, + ); expect(response.json, {'version': libraryVersion}); }); @@ -89,9 +91,9 @@ void main() { 'ok': { 'columnNames': ['?'], 'rows': [ - [123] - ] - } + [123], + ], + }, }); }); @@ -104,8 +106,8 @@ void main() { expect(response.json, { 'ok': { 'raw_tables': [], - 'tables': [containsPair('name', 'users')] - } + 'tables': [containsPair('name', 'users')], + }, }); }); @@ -116,7 +118,8 @@ void main() { isolateId: isolateId, ); - expect(response.json, - {'ok': containsPair('current', containsPair('connected', false))}); + expect(response.json, { + 'ok': containsPair('current', containsPair('connected', false)), + }); }); } diff --git a/packages/powersync/test/disconnect_test.dart b/packages/powersync/test/disconnect_test.dart index e06905f3..4619afe6 100644 --- a/packages/powersync/test/disconnect_test.dart +++ b/packages/powersync/test/disconnect_test.dart @@ -23,10 +23,11 @@ void main() { // A blank endpoint will fail, but that's okay for this test final endpoint = ''; return PowerSyncCredentials( - endpoint: endpoint, - token: 'token', - userId: 'u1', - expiresAt: DateTime.now()); + endpoint: endpoint, + token: 'token', + userId: 'u1', + expiresAt: DateTime.now(), + ); } // ignore: deprecated_member_use_from_same_package @@ -46,8 +47,9 @@ void main() { final db = await testUtils.setupPowerSync(path: path, schema: testSchema); await db.execute( - 'INSERT INTO customers (id, name, email) VALUES(uuid(), ?, ?)', - ['Steven', 'steven@journeyapps.com']); + 'INSERT INTO customers (id, name, email) VALUES(uuid(), ?, ?)', + ['Steven', 'steven@journeyapps.com'], + ); final getCustomersQuery = 'SELECT * from customers'; final initialCustomers = await db.getAll(getCustomersQuery); diff --git a/packages/powersync/test/exceptions_test.dart b/packages/powersync/test/exceptions_test.dart index 62601530..76ed1223 100644 --- a/packages/powersync/test/exceptions_test.dart +++ b/packages/powersync/test/exceptions_test.dart @@ -11,30 +11,39 @@ void main() { test('fromStreamedResponse', () async { final exc = await SyncResponseException.fromStreamedResponse( - StreamedResponse(Stream.value(utf8.encode(errorResponse)), 401)); + StreamedResponse(Stream.value(utf8.encode(errorResponse)), 401), + ); expect(exc.statusCode, 401); - expect(exc.description, - 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + expect( + exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required', + ); }); test('fromResponse', () { - final exc = - SyncResponseException.fromResponse(Response(errorResponse, 401)); + final exc = SyncResponseException.fromResponse( + Response(errorResponse, 401), + ); expect(exc.statusCode, 401); - expect(exc.description, - 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + expect( + exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required', + ); }); test('with description', () { const errorResponse = '{"error":{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError", "details": "Missing authorization header"}}'; - final exc = - SyncResponseException.fromResponse(Response(errorResponse, 401)); + final exc = SyncResponseException.fromResponse( + Response(errorResponse, 401), + ); expect(exc.statusCode, 401); - expect(exc.description, - 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required, Missing authorization header'); + expect( + exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required, Missing authorization header', + ); }); test('malformed', () { @@ -43,12 +52,14 @@ void main() { final exc = SyncResponseException.fromResponse(Response(malformed, 401)); expect(exc.statusCode, 401); - expect(exc.description, - 'Request failed: {"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}'); + expect( + exc.description, + 'Request failed: {"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}', + ); - final exc2 = SyncResponseException.fromResponse(Response( - 'not even json', 500, - reasonPhrase: 'Internal server error')); + final exc2 = SyncResponseException.fromResponse( + Response('not even json', 500, reasonPhrase: 'Internal server error'), + ); expect(exc2.statusCode, 500); expect(exc2.description, 'Internal server error'); }); diff --git a/packages/powersync/test/offline_online_test.dart b/packages/powersync/test/offline_online_test.dart index 1c5c0c98..a33224b5 100644 --- a/packages/powersync/test/offline_online_test.dart +++ b/packages/powersync/test/offline_online_test.dart @@ -45,28 +45,40 @@ Schema makeSchema(bool online) { } final tables = [ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.text('customer_id'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.text('customer_id'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]), + ], + ), Table('customers', [Column.text('name'), Column.text('email')]), ]; return Schema([ for (var table in tables) - Table(table.name, table.columns, - indexes: table.indexes, viewName: onlineName(table.name)), + Table( + table.name, + table.columns, + indexes: table.indexes, + viewName: onlineName(table.name), + ), for (var table in tables) - Table.localOnly('local_${table.name}', table.columns, - indexes: table.indexes, viewName: localName(table.name)) + Table.localOnly( + 'local_${table.name}', + table.columns, + indexes: table.indexes, + viewName: localName(table.name), + ), ]); } @@ -82,19 +94,27 @@ void main() { test('Switch from offline-only to online', () async { // Start with "offline-only" schema. // This does not record any operations to the crud queue. - final db = - await testUtils.setupPowerSync(path: path, schema: makeSchema(false)); + final db = await testUtils.setupPowerSync( + path: path, + schema: makeSchema(false), + ); - await db.execute('INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', - [customerId, 'test customer', 'test@example.org']); await db.execute( - 'INSERT INTO assets(id, description, customer_id) VALUES(?, ?, ?)', - [assetId, 'test', customerId]); - await db - .execute('UPDATE assets SET description = description || ?', ['.']); + 'INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', + [customerId, 'test customer', 'test@example.org'], + ); + await db.execute( + 'INSERT INTO assets(id, description, customer_id) VALUES(?, ?, ?)', + [assetId, 'test', customerId], + ); + await db.execute('UPDATE assets SET description = description || ?', [ + '.', + ]); expect( - await db.getAll('SELECT data FROM ps_crud ORDER BY id'), equals([])); + await db.getAll('SELECT data FROM ps_crud ORDER BY id'), + equals([]), + ); // Now switch to the "online" schema await db.updateSchema(makeSchema(true)); @@ -109,55 +129,65 @@ void main() { // This records each operation to the crud queue. await tx.execute('INSERT INTO customers SELECT * FROM local_customers'); await tx.execute( - 'INSERT INTO assets(id, description, customer_id, user_id) SELECT id, description, customer_id, ? FROM local_assets', - [userId]); + 'INSERT INTO assets(id, description, customer_id, user_id) SELECT id, description, customer_id, ? FROM local_assets', + [userId], + ); // Delete the "offline-only" data. await tx.execute('DELETE FROM local_customers'); await tx.execute('DELETE FROM local_assets'); }); - final crud = (await db.getAll('SELECT data FROM ps_crud ORDER BY id')) - .map((d) => jsonDecode(d['data'] as String)) - .toList(); + final crud = (await db.getAll( + 'SELECT data FROM ps_crud ORDER BY id', + )).map((d) => jsonDecode(d['data'] as String)).toList(); expect( - crud, - equals([ - { - "op": "PUT", - "type": "customers", - "id": customerId, - "data": {"email": "test@example.org", "name": "test customer"} + crud, + equals([ + { + "op": "PUT", + "type": "customers", + "id": customerId, + "data": {"email": "test@example.org", "name": "test customer"}, + }, + { + "op": "PUT", + "type": "assets", + "id": assetId, + "data": { + "user_id": userId, + "customer_id": customerId, + "description": "test.", }, - { - "op": "PUT", - "type": "assets", - "id": assetId, - "data": { - "user_id": userId, - "customer_id": customerId, - "description": "test." - } - } - ])); + }, + ]), + ); }); test('Watch correct table after switching schema', () async { // Start with "offline-only" schema. // This does not record any operations to the crud queue. - var db = - await testUtils.setupPowerSync(path: path, schema: makeSchema(false)); + var db = await testUtils.setupPowerSync( + path: path, + schema: makeSchema(false), + ); - final customerWatchTables = - await getSourceTables(db, 'SELECT * FROM customers'); + final customerWatchTables = await getSourceTables( + db, + 'SELECT * FROM customers', + ); expect( - customerWatchTables.contains('ps_data_local__local_customers'), true); + customerWatchTables.contains('ps_data_local__local_customers'), + true, + ); await db.updateSchema(makeSchema(true)); await db.refreshSchema(); - final onlineCustomerWatchTables = - await getSourceTables(db, 'SELECT * FROM customers'); + final onlineCustomerWatchTables = await getSourceTables( + db, + 'SELECT * FROM customers', + ); expect(onlineCustomerWatchTables.contains('ps_data__customers'), true); }); diff --git a/packages/powersync/test/performance_native_test.dart b/packages/powersync/test/performance_native_test.dart index 324ca08f..4d1374f5 100644 --- a/packages/powersync/test/performance_native_test.dart +++ b/packages/powersync/test/performance_native_test.dart @@ -25,34 +25,40 @@ void main() { // Manual tests test('Insert Performance 3a - computeWithDatabase', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); await db.computeWithDatabase((db) async { for (var i = 0; i < 1000; i++) { db.execute( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Test User', 'user@example.org']); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Test User', 'user@example.org'], + ); } }); print("Completed synchronous inserts in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); test('Insert Performance 3b - prepared statement', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); await db.computeWithDatabase((db) async { var stmt = db.prepare( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)'); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ); try { for (var i = 0; i < 1000; i++) { stmt.execute(['Test User', 'user@example.org']); @@ -63,59 +69,73 @@ void main() { }); print("Completed synchronous inserts prepared in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); - test('Insert Performance 3c - prepared statement, dart-generated ids', - () async { - final db = PowerSyncDatabase.withFactory( + test( + 'Insert Performance 3c - prepared statement, dart-generated ids', + () async { + final db = PowerSyncDatabase.withFactory( await testUtils.testFactory(path: path), - schema: pschema); - await db.initialize(); - // Test to exclude the function overhead time of generating uuids - final timer = Stopwatch()..start(); - - await db.computeWithDatabase((db) async { + schema: pschema, + ); + await db.initialize(); + // Test to exclude the function overhead time of generating uuids + final timer = Stopwatch()..start(); + + await db.computeWithDatabase((db) async { + var ids = List.generate(1000, (index) => uuid.v4()); + var stmt = db.prepare( + 'INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', + ); + try { + for (var id in ids) { + stmt.execute([id, 'Test User', 'user@example.org']); + } + } finally { + stmt.close(); + } + }); + + print("Completed synchronous inserts prepared in ${timer.elapsed}"); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); + }, + ); + test( + 'Insert Performance 3d - prepared statement, pre-generated ids', + () async { + final db = PowerSyncDatabase.withFactory( + await testUtils.testFactory(path: path), + schema: pschema, + ); + await db.initialize(); + // Test to completely exclude time taken to generate uuids var ids = List.generate(1000, (index) => uuid.v4()); - var stmt = db - .prepare('INSERT INTO customers(id, name, email) VALUES(?, ?, ?)'); - try { + + final timer = Stopwatch()..start(); + + await db.computeWithDatabase((db) async { + var stmt = db.prepare( + 'INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', + ); for (var id in ids) { stmt.execute([id, 'Test User', 'user@example.org']); } - } finally { stmt.close(); - } - }); - - print("Completed synchronous inserts prepared in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); - }); - test('Insert Performance 3d - prepared statement, pre-generated ids', - () async { - final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); - await db.initialize(); - // Test to completely exclude time taken to generate uuids - var ids = List.generate(1000, (index) => uuid.v4()); - - final timer = Stopwatch()..start(); - - await db.computeWithDatabase((db) async { - var stmt = db - .prepare('INSERT INTO customers(id, name, email) VALUES(?, ?, ?)'); - for (var id in ids) { - stmt.execute([id, 'Test User', 'user@example.org']); - } - stmt.close(); - }); - - print("Completed synchronous inserts prepared in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); - }); + }); + + print("Completed synchronous inserts prepared in ${timer.elapsed}"); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); + }, + ); }); } diff --git a/packages/powersync/test/performance_shared_test.dart b/packages/powersync/test/performance_shared_test.dart index d2970230..5183878a 100644 --- a/packages/powersync/test/performance_shared_test.dart +++ b/packages/powersync/test/performance_shared_test.dart @@ -16,7 +16,7 @@ const pschema = Schema([ Column.text('customer_id'), Column.text('description'), ]), - Table.localOnly('customers', [Column.text('name'), Column.text('email')]) + Table.localOnly('customers', [Column.text('name'), Column.text('email')]), ]); void main() { @@ -35,44 +35,53 @@ void main() { // Manual tests test('Insert Performance 1 - direct', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); for (var i = 0; i < 1000; i++) { await db.execute( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Test User', 'user@example.org']); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Test User', 'user@example.org'], + ); } print("Completed sequential inserts in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); test('Insert Performance 2 - writeTransaction', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); await db.writeTransaction((tx) async { for (var i = 0; i < 1000; i++) { await tx.execute( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Test User', 'user@example.org']); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Test User', 'user@example.org'], + ); } }); print("Completed transaction inserts in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); test('Insert Performance 4 - pipelined', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); @@ -80,31 +89,41 @@ void main() { List> futures = []; for (var i = 0; i < 1000; i++) { var future = tx.execute( - 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', - ['Test User', 'user@example.org']); + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Test User', 'user@example.org'], + ); futures.add(future); } await Future.wait(futures); }); print("Completed pipelined inserts in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); test('Insert Performance 5 - executeBatch', () async { final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: pschema); + await testUtils.testFactory(path: path), + schema: pschema, + ); await db.initialize(); final timer = Stopwatch()..start(); var parameters = List.generate( - 1000, (index) => [uuid.v4(), 'Test user', 'user@example.org']); + 1000, + (index) => [uuid.v4(), 'Test user', 'user@example.org'], + ); await db.executeBatch( - 'INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', parameters); + 'INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', + parameters, + ); print("Completed executeBatch in ${timer.elapsed}"); - expect(await db.get('SELECT count(*) as count FROM customers'), - equals({'count': 1000})); + expect( + await db.get('SELECT count(*) as count FROM customers'), + equals({'count': 1000}), + ); }); }); } diff --git a/packages/powersync/test/powersync_native_test.dart b/packages/powersync/test/powersync_native_test.dart index b447d56f..11f1867c 100644 --- a/packages/powersync/test/powersync_native_test.dart +++ b/packages/powersync/test/powersync_native_test.dart @@ -27,26 +27,31 @@ void main() { test('Basic Setup', () async { final db = await testUtils.setupPowerSync(path: path); - await db.execute( - 'INSERT INTO assets(id, make) VALUES(uuid(), ?)', ['Test Make']); + await db.execute('INSERT INTO assets(id, make) VALUES(uuid(), ?)', [ + 'Test Make', + ]); final result = await db.get('SELECT make FROM assets'); expect(result, equals({'make': 'Test Make'})); expect( - await db.execute('PRAGMA journal_mode'), - equals([ - {'journal_mode': 'wal'} - ])); + await db.execute('PRAGMA journal_mode'), + equals([ + {'journal_mode': 'wal'}, + ]), + ); expect( - await db.execute('PRAGMA locking_mode'), - equals([ - {'locking_mode': 'normal'} - ])); + await db.execute('PRAGMA locking_mode'), + equals([ + {'locking_mode': 'normal'}, + ]), + ); }); test('Concurrency', () async { final db = PowerSyncDatabase.withFactory( await testUtils.testFactory( - path: path, options: SqliteOptions(maxReaders: 3)), + path: path, + options: SqliteOptions(maxReaders: 3), + ), schema: defaultSchema, ); addTearDown(db.close); @@ -84,68 +89,79 @@ void main() { await db.getAll("WITH test AS (SELECT 1 AS one) SELECT * FROM test"); // Cannot write - await expectLater(() async { - await db.getAll('INSERT INTO assets(id) VALUES(?)', ['test']); - }, - throwsA((dynamic e) => + await expectLater( + () async { + await db.getAll('INSERT INTO assets(id) VALUES(?)', ['test']); + }, + throwsA( + (dynamic e) => e is SqliteException && - e.message.contains('attempt to write a readonly database'))); + e.message.contains('attempt to write a readonly database'), + ), + ); // Can use WITH ... SELECT await db.getAll("WITH test AS (SELECT 1 AS one) SELECT * FROM test"); // Cannot use WITH .... INSERT - await expectLater(() async { - await db.getAll( - "WITH test AS (SELECT 1 AS one) INSERT INTO assets(id) SELECT one FROM test"); - }, - throwsA((dynamic e) => + await expectLater( + () async { + await db.getAll( + "WITH test AS (SELECT 1 AS one) INSERT INTO assets(id) SELECT one FROM test", + ); + }, + throwsA( + (dynamic e) => e is SqliteException && - e.message.contains('attempt to write a readonly database'))); + e.message.contains('attempt to write a readonly database'), + ), + ); await db.writeTransaction((tx) async { // Within a write transaction, this is fine - await tx - .getAll('INSERT INTO assets(id) VALUES(?) RETURNING *', ['test']); + await tx.getAll('INSERT INTO assets(id) VALUES(?) RETURNING *', [ + 'test', + ]); }); }); test( - 'should allow read-only db calls within transaction callback in separate zone', - () async { - final db = await testUtils.setupPowerSync(path: path); + 'should allow read-only db calls within transaction callback in separate zone', + () async { + final db = await testUtils.setupPowerSync(path: path); - // Get a reference to the parent zone (outside the transaction). - final zone = Zone.current; + // Get a reference to the parent zone (outside the transaction). + final zone = Zone.current; - // Each of these are fine, since it could use a separate connection. - // Note: In highly concurrent cases, it could exhaust the connection pool and cause a deadlock. + // Each of these are fine, since it could use a separate connection. + // Note: In highly concurrent cases, it could exhaust the connection pool and cause a deadlock. - await db.writeTransaction((tx) async { - // Use the parent zone to avoid the "recursive lock" error. - await zone.fork().run(() async { - await db.getAll('SELECT * FROM assets'); + await db.writeTransaction((tx) async { + // Use the parent zone to avoid the "recursive lock" error. + await zone.fork().run(() async { + await db.getAll('SELECT * FROM assets'); + }); }); - }); - await db.readTransaction((tx) async { - await zone.fork().run(() async { - await db.getAll('SELECT * FROM assets'); + await db.readTransaction((tx) async { + await zone.fork().run(() async { + await db.getAll('SELECT * FROM assets'); + }); }); - }); - await db.readTransaction((tx) async { - await zone.fork().run(() async { - await db.execute('SELECT * FROM assets'); + await db.readTransaction((tx) async { + await zone.fork().run(() async { + await db.execute('SELECT * FROM assets'); + }); }); - }); - // Note: This would deadlock, since it shares a global write lock. - // await db.writeTransaction((tx) async { - // await zone.fork().run(() async { - // await db.execute('SELECT * FROM test_data'); - // }); - // }); - }); + // Note: This would deadlock, since it shares a global write lock. + // await db.writeTransaction((tx) async { + // await zone.fork().run(() async { + // await db.execute('SELECT * FROM test_data'); + // }); + // }); + }, + ); }); } diff --git a/packages/powersync/test/powersync_shared_test.dart b/packages/powersync/test/powersync_shared_test.dart index d362f28a..d546d677 100644 --- a/packages/powersync/test/powersync_shared_test.dart +++ b/packages/powersync/test/powersync_shared_test.dart @@ -28,13 +28,17 @@ void main() { final subscription = logger.onRecord.listen(events.add); addTearDown(subscription.cancel); - final firstInstance = - await testUtils.setupPowerSync(path: path, logger: logger); + final firstInstance = await testUtils.setupPowerSync( + path: path, + logger: logger, + ); await firstInstance.initialize(); expect(events, isEmpty); - final secondInstance = - await testUtils.setupPowerSync(path: path, logger: logger); + final secondInstance = await testUtils.setupPowerSync( + path: path, + logger: logger, + ); await secondInstance.initialize(); expect( events, @@ -43,71 +47,93 @@ void main() { (e) => e.message, 'message', contains( - 'Multiple instances for the same database have been detected.'), + 'Multiple instances for the same database have been detected.', + ), ), ), ); }); - test('should not allow direct db calls within a transaction callback', - () async { - final db = await testUtils.setupPowerSync(path: path); - - await db.writeTransaction((tx) async { - await expectLater(() async { - await db.execute('INSERT INTO assets(id) VALUES(?)', ['test']); - }, - throwsA((dynamic e) => - e is LockError && e.message.contains('tx.execute'))); - }); - }); - - test('should not allow read-only db calls within transaction callback', - () async { - final db = await testUtils.setupPowerSync(path: path); - - await db.writeTransaction((tx) async { - // This uses a different connection, so it _could_ work. - // But it's likely unintentional and could cause weird bugs, so we don't - // allow it by default. - await expectLater(() async { - await db.getAll('SELECT * FROM assets'); - }, - throwsA((dynamic e) => - e is LockError && e.message.contains('tx.getAll'))); - }); - - await db.readTransaction((tx) async { - // This does actually attempt a lock on the same connection, so it - // errors. - // This also exposes an interesting test case where the read transaction - // opens another connection, but doesn't use it. - await expectLater(() async { - await db.getAll('SELECT * FROM assets'); - }, - throwsA((dynamic e) => - e is LockError && e.message.contains('tx.getAll'))); - }); - }); + test( + 'should not allow direct db calls within a transaction callback', + () async { + final db = await testUtils.setupPowerSync(path: path); + + await db.writeTransaction((tx) async { + await expectLater( + () async { + await db.execute('INSERT INTO assets(id) VALUES(?)', ['test']); + }, + throwsA( + (dynamic e) => e is LockError && e.message.contains('tx.execute'), + ), + ); + }); + }, + ); + + test( + 'should not allow read-only db calls within transaction callback', + () async { + final db = await testUtils.setupPowerSync(path: path); + + await db.writeTransaction((tx) async { + // This uses a different connection, so it _could_ work. + // But it's likely unintentional and could cause weird bugs, so we don't + // allow it by default. + await expectLater( + () async { + await db.getAll('SELECT * FROM assets'); + }, + throwsA( + (dynamic e) => e is LockError && e.message.contains('tx.getAll'), + ), + ); + }); + + await db.readTransaction((tx) async { + // This does actually attempt a lock on the same connection, so it + // errors. + // This also exposes an interesting test case where the read transaction + // opens another connection, but doesn't use it. + await expectLater( + () async { + await db.getAll('SELECT * FROM assets'); + }, + throwsA( + (dynamic e) => e is LockError && e.message.contains('tx.getAll'), + ), + ); + }); + }, + ); test('should not allow read-only db calls within lock callback', () async { final db = await testUtils.setupPowerSync(path: path); // Locks - should behave the same as transactions above await db.writeLock((tx) async { - await expectLater(() async { - await db.getOptional('SELECT * FROM assets'); - }, - throwsA((dynamic e) => - e is LockError && e.message.contains('tx.getOptional'))); + await expectLater( + () async { + await db.getOptional('SELECT * FROM assets'); + }, + throwsA( + (dynamic e) => + e is LockError && e.message.contains('tx.getOptional'), + ), + ); }); await db.readLock((tx) async { - await expectLater(() async { - await db.getOptional('SELECT * FROM assets'); - }, - throwsA((dynamic e) => - e is LockError && e.message.contains('tx.getOptional'))); + await expectLater( + () async { + await db.getOptional('SELECT * FROM assets'); + }, + throwsA( + (dynamic e) => + e is LockError && e.message.contains('tx.getOptional'), + ), + ); }); }); @@ -130,21 +156,20 @@ void main() { final db = await testUtils.setupPowerSync(path: path); expectLater( db.statusStream, - emitsInOrder( - [ - // Manual setStatus call. hasSynced set to true because lastSyncedAt is set - isA().having((e) => e.hasSynced, 'hasSynced', true), - // Closing the database emits a disconnected status - isA().having((e) => e.connected, 'connected', false), - emitsDone - ], - ), + emitsInOrder([ + // Manual setStatus call. hasSynced set to true because lastSyncedAt is set + isA().having((e) => e.hasSynced, 'hasSynced', true), + // Closing the database emits a disconnected status + isA().having((e) => e.connected, 'connected', false), + emitsDone, + ]), ); - final status = (MutableSyncStatus() - ..connected = true - ..lastSyncedAt = DateTime.now()) - .immutableSnapshot(); + final status = + (MutableSyncStatus() + ..connected = true + ..lastSyncedAt = DateTime.now()) + .immutableSnapshot(); db.setStatus(status); db.setStatus(status); // Should not re-emit! @@ -153,18 +178,25 @@ void main() { test('can clear raw tables', () async { final db = await testUtils.setupPowerSync(path: path); - await db.updateSchema(const Schema([], rawTables: [ - RawTable( - name: 'unused', - put: PendingStatement(sql: '', params: []), - delete: PendingStatement(sql: '', params: []), - clear: 'DELETE FROM lists', - ) - ])); + await db.updateSchema( + const Schema( + [], + rawTables: [ + RawTable( + name: 'unused', + put: PendingStatement(sql: '', params: []), + delete: PendingStatement(sql: '', params: []), + clear: 'DELETE FROM lists', + ), + ], + ), + ); await db.execute( - 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT)'); - await db - .execute('INSERT INTO lists (id, name) VALUES (uuid(), ?)', ['list']); + 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT)', + ); + await db.execute('INSERT INTO lists (id, name) VALUES (uuid(), ?)', [ + 'list', + ]); expect(await db.getAll('SELECT * FROM lists'), hasLength(1)); await db.disconnectAndClear(); diff --git a/packages/powersync/test/schema_test.dart b/packages/powersync/test/schema_test.dart index f9ccd230..de163a21 100644 --- a/packages/powersync/test/schema_test.dart +++ b/packages/powersync/test/schema_test.dart @@ -7,22 +7,26 @@ final testUtils = TestUtils(); const testId = "2290de4f-0488-4e50-abed-f8e8eb1d0b42"; final schema = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.real('weight'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weight'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]), + ], + ), Table('customers', [Column.text('name'), Column.text('email')]), Table.insertOnly('logs', [Column.text('level'), Column.text('content')]), Table.localOnly('credentials', [Column.text('key'), Column.text('value')]), - Table('aliased', [Column.text('name')], viewName: 'test1') + Table('aliased', [Column.text('name')], viewName: 'test1'), ]); void main() { @@ -38,36 +42,48 @@ void main() { // Test that powersync_replace_schema() is a no-op when the schema is not // modified. - final powersync = - await testUtils.setupPowerSync(path: path, schema: schema); + final powersync = await testUtils.setupPowerSync( + path: path, + schema: schema, + ); final versionBefore = await powersync.get('PRAGMA schema_version'); await powersync.updateSchema(schema); final versionAfter = await powersync.get('PRAGMA schema_version'); // No change - expect(versionAfter['schema_version'], - equals(versionBefore['schema_version'])); + expect( + versionAfter['schema_version'], + equals(versionBefore['schema_version']), + ); final schema2 = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.real('weights'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weights'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]), + ], + ), Table('customers', [Column.text('name'), Column.text('email')]), - Table.insertOnly( - 'logs', [Column.text('level'), Column.text('content')]), - Table.localOnly( - 'credentials', [Column.text('key'), Column.text('value')]), - Table('aliased', [Column.text('name')], viewName: 'test1') + Table.insertOnly('logs', [ + Column.text('level'), + Column.text('content'), + ]), + Table.localOnly('credentials', [ + Column.text('key'), + Column.text('value'), + ]), + Table('aliased', [Column.text('name')], viewName: 'test1'), ]); await powersync.updateSchema(schema2); @@ -75,29 +91,41 @@ void main() { final versionAfter2 = await powersync.get('PRAGMA schema_version'); // Updated - expect(versionAfter2['schema_version'], - greaterThan(versionAfter['schema_version'] as int)); + expect( + versionAfter2['schema_version'], + greaterThan(versionAfter['schema_version'] as int), + ); final schema3 = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.real('weights'), - Column.text('description'), - ], indexes: [ - Index('makemodel', - [IndexedColumn('make'), IndexedColumn.descending('model')]) - ]), + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.real('weights'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [ + IndexedColumn('make'), + IndexedColumn.descending('model'), + ]), + ], + ), Table('customers', [Column.text('name'), Column.text('email')]), - Table.insertOnly( - 'logs', [Column.text('level'), Column.text('content')]), - Table.localOnly( - 'credentials', [Column.text('key'), Column.text('value')]), - Table('aliased', [Column.text('name')], viewName: 'test1') + Table.insertOnly('logs', [ + Column.text('level'), + Column.text('content'), + ]), + Table.localOnly('credentials', [ + Column.text('key'), + Column.text('value'), + ]), + Table('aliased', [Column.text('name')], viewName: 'test1'), ]); await powersync.updateSchema(schema3); @@ -105,20 +133,28 @@ void main() { final versionAfter3 = await powersync.get('PRAGMA schema_version'); // Updated again (index) - expect(versionAfter3['schema_version'], - greaterThan(versionAfter2['schema_version'] as int)); + expect( + versionAfter3['schema_version'], + greaterThan(versionAfter2['schema_version'] as int), + ); }); /// The assets table is locked after performing the EXPLAIN QUERY test('Indexing', () async { - final powersync = - await testUtils.setupPowerSync(path: path, schema: schema); + final powersync = await testUtils.setupPowerSync( + path: path, + schema: schema, + ); final results = await powersync.execute( - 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']); + 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', + ['test'], + ); - expect(results[0]['detail'], - contains('USING INDEX ps_data__assets__makemodel')); + expect( + results[0]['detail'], + contains('USING INDEX ps_data__assets__makemodel'), + ); // Now drop the index final schema2 = Schema([ @@ -138,48 +174,55 @@ void main() { // Execute instead of getAll so that we don't get a cached query plan // from a different connection final results2 = await powersync.execute( - 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', ['test']); + 'EXPLAIN QUERY PLAN SELECT * FROM assets WHERE make = ?', + ['test'], + ); expect(results2[0]['detail'], contains('SCAN')); }); test('Validation runs on setup', () async { final schema = Schema([ - Table('#assets', [ - Column.text('name'), - ]), + Table('#assets', [Column.text('name')]), ]); try { await testUtils.setupPowerSync(path: path, schema: schema); } catch (e) { expect( - e, - isA().having((e) => e.message, 'message', - 'Invalid characters in table name: #assets')); + e, + isA().having( + (e) => e.message, + 'message', + 'Invalid characters in table name: #assets', + ), + ); } }); test('Validation runs on update', () async { final schema = Schema([ - Table('works', [ - Column.text('name'), - ]), + Table('works', [Column.text('name')]), ]); - final powersync = - await testUtils.setupPowerSync(path: path, schema: schema); + final powersync = await testUtils.setupPowerSync( + path: path, + schema: schema, + ); final schema2 = Schema([ - Table('#notworking', [ - Column.text('created_at'), - ]), + Table('#notworking', [Column.text('created_at')]), ]); await expectLater( () => powersync.updateSchema(schema2), - throwsA(isA().having((e) => e.message, 'message', - 'Invalid characters in table name: #notworking')), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Invalid characters in table name: #notworking', + ), + ), ); }); }); @@ -200,12 +243,9 @@ void main() { }); test('Create a local-only table', () { - final table = Table.localOnly( - 'local_users', - [ - Column('name', ColumnType.text), - ], - viewName: 'local_user_view'); + final table = Table.localOnly('local_users', [ + Column('name', ColumnType.text), + ], viewName: 'local_user_view'); expect(table.name, equals('local_users')); expect(table.localOnly, isTrue); @@ -239,8 +279,9 @@ void main() { }); test('Validate table name', () { - final invalidTableName = - Table('#invalid_table_name', [Column('name', ColumnType.text)]); + final invalidTableName = Table('#invalid_table_name', [ + Column('name', ColumnType.text), + ]); expect( () => invalidTableName.validate(), @@ -255,9 +296,9 @@ void main() { }); test('Validate view name', () { - final invalidTableName = Table( - 'valid_table_name', [Column('name', ColumnType.text)], - viewName: '#invalid_view_name'); + final invalidTableName = Table('valid_table_name', [ + Column('name', ColumnType.text), + ], viewName: '#invalid_view_name'); expect( () => invalidTableName.validate(), @@ -319,33 +360,49 @@ void main() { }); test('local-only with metadata', () { - final table = Table('foo', [Column.text('bar')], - localOnly: true, trackMetadata: true); + final table = Table( + 'foo', + [Column.text('bar')], + localOnly: true, + trackMetadata: true, + ); expect( - table.validate, - throwsA(isA().having((e) => e.message, 'emssage', - "Local-only tables can't track metadata"))); + table.validate, + throwsA( + isA().having( + (e) => e.message, + 'emssage', + "Local-only tables can't track metadata", + ), + ), + ); }); test('local-only with trackPreviousValues', () { - final table = Table('foo', [Column.text('bar')], - localOnly: true, trackPreviousValues: TrackPreviousValuesOptions()); + final table = Table( + 'foo', + [Column.text('bar')], + localOnly: true, + trackPreviousValues: TrackPreviousValuesOptions(), + ); expect( - table.validate, - throwsA(isA().having((e) => e.message, 'emssage', - "Local-only tables can't track old values"))); + table.validate, + throwsA( + isA().having( + (e) => e.message, + 'emssage', + "Local-only tables can't track old values", + ), + ), + ); }); test('Schema without duplicate table names', () { final schema = Schema([ - Table('duplicate', [ - Column.text('name'), - ]), - Table('not_duplicate', [ - Column.text('name'), - ]), + Table('duplicate', [Column.text('name')]), + Table('not_duplicate', [Column.text('name')]), ]); expect(() => schema.validate(), returnsNormally); @@ -353,12 +410,8 @@ void main() { test('Schema with duplicate table names', () { final schema = Schema([ - Table('clone', [ - Column.text('name'), - ]), - Table('clone', [ - Column.text('name'), - ]), + Table('clone', [Column.text('name')]), + Table('clone', [Column.text('name')]), ]); expect( @@ -374,12 +427,13 @@ void main() { }); test('toJson method', () { - final table = Table('users', [ - Column('name', ColumnType.text), - Column('age', ColumnType.integer), - ], indexes: [ - Index('name_index', [IndexedColumn('name')]) - ]); + final table = Table( + 'users', + [Column('name', ColumnType.text), Column('age', ColumnType.integer)], + indexes: [ + Index('name_index', [IndexedColumn('name')]), + ], + ); final json = table.toJson(); expect(json, { @@ -395,15 +449,22 @@ void main() { }); test('handles options', () { - expect(Table('foo', [], trackMetadata: true).toJson(), - containsPair('include_metadata', isTrue)); + expect( + Table('foo', [], trackMetadata: true).toJson(), + containsPair('include_metadata', isTrue), + ); - expect(Table('foo', [], ignoreEmptyUpdates: true).toJson(), - containsPair('ignore_empty_update', isTrue)); + expect( + Table('foo', [], ignoreEmptyUpdates: true).toJson(), + containsPair('ignore_empty_update', isTrue), + ); expect( - Table('foo', [], trackPreviousValues: TrackPreviousValuesOptions()) - .toJson(), + Table( + 'foo', + [], + trackPreviousValues: TrackPreviousValuesOptions(), + ).toJson(), allOf( containsPair('include_old', isTrue), containsPair('include_old_only_when_changed', isFalse), @@ -411,10 +472,13 @@ void main() { ); expect( - Table('foo', [], - trackPreviousValues: - TrackPreviousValuesOptions(columnFilter: ['foo', 'bar'])) - .toJson(), + Table( + 'foo', + [], + trackPreviousValues: TrackPreviousValuesOptions( + columnFilter: ['foo', 'bar'], + ), + ).toJson(), allOf( containsPair('include_old', ['foo', 'bar']), containsPair('include_old_only_when_changed', isFalse), @@ -422,10 +486,13 @@ void main() { ); expect( - Table('foo', [], - trackPreviousValues: - TrackPreviousValuesOptions(onlyWhenChanged: true)) - .toJson(), + Table( + 'foo', + [], + trackPreviousValues: TrackPreviousValuesOptions( + onlyWhenChanged: true, + ), + ).toJson(), allOf( containsPair('include_old', isTrue), containsPair('include_old_only_when_changed', isTrue), diff --git a/packages/powersync/test/server/asset_server.dart b/packages/powersync/test/server/asset_server.dart index 272e0bfa..4880c2e2 100644 --- a/packages/powersync/test/server/asset_server.dart +++ b/packages/powersync/test/server/asset_server.dart @@ -22,7 +22,9 @@ Middleware cors() { } return createMiddleware( - requestHandler: handleOptionsRequest, responseHandler: addCorsHeaders); + requestHandler: handleOptionsRequest, + responseHandler: addCorsHeaders, + ); } Future hybridMain(StreamChannel channel) async { diff --git a/packages/powersync/test/server/sync_server/in_memory_sync_server.dart b/packages/powersync/test/server/sync_server/in_memory_sync_server.dart index 278f014e..287646cd 100644 --- a/packages/powersync/test/server/sync_server/in_memory_sync_server.dart +++ b/packages/powersync/test/server/sync_server/in_memory_sync_server.dart @@ -10,14 +10,14 @@ final class MockSyncService { final bool useBson; // Use a queued stream to make tests easier. - StreamController controller = + StreamController controller = StreamController(); Completer _listener = Completer(); var router = Router(); Object? Function() writeCheckpoint = () { return { - 'data': {'write_checkpoint': '10'} + 'data': {'write_checkpoint': '10'}, }; }; @@ -25,8 +25,9 @@ final class MockSyncService { router ..post('/sync/stream', (Request request) async { if (useBson && - !request.headers['Accept']! - .contains('application/vnd.powersync.bson-stream')) { + !request.headers['Accept']!.contains( + 'application/vnd.powersync.bson-stream', + )) { throw "Want to serve bson, but client doesn't accept it"; } @@ -40,20 +41,23 @@ final class MockSyncService { }; }); - return Response.ok(bytes, headers: { - 'Content-Type': useBson - ? 'application/vnd.powersync.bson-stream' - : 'application/x-ndjson', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, context: { - "shelf.io.buffer_output": false - }); + return Response.ok( + bytes, + headers: { + 'Content-Type': useBson + ? 'application/vnd.powersync.bson-stream' + : 'application/x-ndjson', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + context: {"shelf.io.buffer_output": false}, + ); }) ..get('/write-checkpoint2.json', (request) { - return Response.ok(json.encode(writeCheckpoint()), headers: { - 'Content-Type': 'application/json', - }); + return Response.ok( + json.encode(writeCheckpoint()), + headers: {'Content-Type': 'application/json'}, + ); }); } diff --git a/packages/powersync/test/server/worker_server.dart b/packages/powersync/test/server/worker_server.dart index 366f3a02..d9c28319 100644 --- a/packages/powersync/test/server/worker_server.dart +++ b/packages/powersync/test/server/worker_server.dart @@ -15,14 +15,16 @@ Future hybridMain(StreamChannel channel) async { if (!(await File(sqliteOutputPath).exists())) { throw AssertionError( - 'sqlite3.wasm file should be present in the powersync/assets folder'); + 'sqlite3.wasm file should be present in the powersync/assets folder', + ); } final workerOutputPath = p.join(assetsDirectory, 'powersync_db.worker.js'); if (!(await File(workerOutputPath).exists())) { throw AssertionError( - 'powersync_db.worker.js file should be present in the powersync/assets folder'); + 'powersync_db.worker.js file should be present in the powersync/assets folder', + ); } final server = await HttpServer.bind('localhost', 0); diff --git a/packages/powersync/test/stream_test.dart b/packages/powersync/test/stream_test.dart index 90ffe2cb..2d12554a 100644 --- a/packages/powersync/test/stream_test.dart +++ b/packages/powersync/test/stream_test.dart @@ -16,8 +16,12 @@ void main() { tearDown(() async {}); - Stream genStream(String prefix, Duration delay, - [int count = 50, Object? error]) async* { + Stream genStream( + String prefix, + Duration delay, [ + int count = 50, + Object? error, + ]) async* { for (var i = 0; i < count; i++) { yield "$prefix $i"; await Future.delayed(delay); @@ -29,16 +33,22 @@ void main() { test('addBroadcast - basic', () async { Stream stream1 = genStream('S1:', Duration(milliseconds: 5)); - Stream stream2 = - genStream('S2:', Duration(milliseconds: 20)).asBroadcastStream(); + Stream stream2 = genStream( + 'S2:', + Duration(milliseconds: 20), + ).asBroadcastStream(); var merged = addBroadcast(stream1, stream2); var data = await merged.take(20).toList(); - var countS1 = - data.where((element) => element.startsWith('S1')).toList().length; - var countS2 = - data.where((element) => element.startsWith('S2')).toList().length; + var countS1 = data + .where((element) => element.startsWith('S1')) + .toList() + .length; + var countS2 = data + .where((element) => element.startsWith('S2')) + .toList() + .length; expect(countS1 + countS2, equals(20)); expect(countS1, greaterThanOrEqualTo(10)); expect(countS2, greaterThanOrEqualTo(0)); @@ -46,10 +56,16 @@ void main() { test('addBroadcast - errors', () async { Object simulatedError = AssertionError('Closed'); - Stream stream1 = - genStream('S1:', Duration(milliseconds: 5), 5, simulatedError); - Stream stream2 = - genStream('S2:', Duration(milliseconds: 20)).asBroadcastStream(); + Stream stream1 = genStream( + 'S1:', + Duration(milliseconds: 5), + 5, + simulatedError, + ); + Stream stream2 = genStream( + 'S2:', + Duration(milliseconds: 20), + ).asBroadcastStream(); var merged = addBroadcast(stream1, stream2); @@ -68,8 +84,10 @@ void main() { test('addBroadcast - errors on cancel', () async { Object simulatedError = AssertionError('Closed'); - Stream stream2 = - genStream('S2:', Duration(milliseconds: 20)).asBroadcastStream(); + Stream stream2 = genStream( + 'S2:', + Duration(milliseconds: 20), + ).asBroadcastStream(); var controller = StreamController(); var stream1 = controller.stream; @@ -99,10 +117,16 @@ void main() { test('addBroadcast - re-use broadcast after error', () async { Object simulatedError = AssertionError('Closed'); - Stream stream1 = - genStream('S1:', Duration(milliseconds: 5), 5, simulatedError); - Stream sb = - genStream('SB:', Duration(milliseconds: 20)).asBroadcastStream(); + Stream stream1 = genStream( + 'S1:', + Duration(milliseconds: 5), + 5, + simulatedError, + ); + Stream sb = genStream( + 'SB:', + Duration(milliseconds: 20), + ).asBroadcastStream(); var merged = addBroadcast(stream1, sb); @@ -123,10 +147,14 @@ void main() { var merged2 = addBroadcast(stream3, sb); var data = await merged2.take(20).toList(); - var countS1 = - data.where((element) => element.startsWith('S3')).toList().length; - var countS2 = - data.where((element) => element.startsWith('SB')).toList().length; + var countS1 = data + .where((element) => element.startsWith('S3')) + .toList() + .length; + var countS2 = data + .where((element) => element.startsWith('SB')) + .toList() + .length; expect(countS1 + countS2, equals(20)); expect(countS1, greaterThanOrEqualTo(10)); expect(countS2, greaterThanOrEqualTo(0)); @@ -157,11 +185,12 @@ void main() { var parsedStream = sourceStream.lines.parseJson; var data = await parsedStream.toList(); expect( - data, - equals([ - {"line": 1}, - {"line": 2} - ])); + data, + equals([ + {"line": 1}, + {"line": 2}, + ]), + ); }); test('ndjson over Pipe', () async { @@ -178,11 +207,12 @@ void main() { var parsedStream = ByteStream(pipe.read).lines.parseJson; var data = await parsedStream.toList(); expect( - data, - equals([ - {"line": 1}, - {"line": 2} - ])); + data, + equals([ + {"line": 1}, + {"line": 2}, + ]), + ); }); test('ndjson with partial data', () async { @@ -206,12 +236,15 @@ void main() { error = e; } expect( - result, - equals([ - {"line": 1} - ])); - expect(error.toString(), - startsWith('FormatException: Unexpected end of input')); + result, + equals([ + {"line": 1}, + ]), + ); + expect( + error.toString(), + startsWith('FormatException: Unexpected end of input'), + ); }); test('ndjson with partial data and merged stream', () async { @@ -225,8 +258,10 @@ void main() { writer(); var parsedStream = ByteStream(pipe.read).lines.parseJson; - Stream stream2 = - genStream('S2:', Duration(milliseconds: 50)).asBroadcastStream(); + Stream stream2 = genStream( + 'S2:', + Duration(milliseconds: 50), + ).asBroadcastStream(); var merged = addBroadcast(parsedStream, stream2); @@ -240,13 +275,16 @@ void main() { error = e; } expect( - result, - equals([ - 'S2: 0', - {"line": 1} - ])); - expect(error.toString(), - startsWith('FormatException: Unexpected end of input')); + result, + equals([ + 'S2: 0', + {"line": 1}, + ]), + ); + expect( + error.toString(), + startsWith('FormatException: Unexpected end of input'), + ); }); }); } diff --git a/packages/powersync/test/sync/custom_client_test.dart b/packages/powersync/test/sync/custom_client_test.dart index 8418f7f4..26de6976 100644 --- a/packages/powersync/test/sync/custom_client_test.dart +++ b/packages/powersync/test/sync/custom_client_test.dart @@ -25,11 +25,11 @@ void main() { await powersync.connect( connector: TestConnector( () async => PowerSyncCredentials( - endpoint: 'http://test.powersync.example.org', token: 'token'), - ), - options: SyncOptions( - httpClient: _createMockClient, + endpoint: 'http://test.powersync.example.org', + token: 'token', + ), ), + options: SyncOptions(httpClient: _createMockClient), ); await powersync.waitForFirstSync(); diff --git a/packages/powersync/test/sync/in_memory_sync_test.dart b/packages/powersync/test/sync/in_memory_sync_test.dart index 6a3f2469..d6d65bb1 100644 --- a/packages/powersync/test/sync/in_memory_sync_test.dart +++ b/packages/powersync/test/sync/in_memory_sync_test.dart @@ -50,17 +50,14 @@ void _declareTests(String name, SyncOptions options, bool bson) { database.httpClient = client; await database.connect( - connector: TestConnector( - () async { - credentialsCallbackCount++; - return PowerSyncCredentials( - endpoint: server.url.toString(), - token: 'token$credentialsCallbackCount', - expiresAt: DateTime.now(), - ); - }, - uploadData: (db) => uploadData(db), - ), + connector: TestConnector(() async { + credentialsCallbackCount++; + return PowerSyncCredentials( + endpoint: server.url.toString(), + token: 'token$credentialsCallbackCount', + expiresAt: DateTime.now(), + ); + }, uploadData: (db) => uploadData(db)), options: options, ); } @@ -79,8 +76,10 @@ void _declareTests(String name, SyncOptions options, bool bson) { await syncService.stop(); }); - Future> waitForConnection( - {bool expectNoWarnings = true, bool addKeepLive = true}) async { + Future> waitForConnection({ + bool expectNoWarnings = true, + bool addKeepLive = true, + }) async { if (expectNoWarnings) { logger.onRecord.listen((e) { if (e.level >= Level.WARNING) { @@ -100,8 +99,10 @@ void _declareTests(String name, SyncOptions options, bool bson) { syncService.addKeepAlive(); } - await expectLater(status, - emitsThrough(isSyncStatus(connected: true, hasSynced: false))); + await expectLater( + status, + emitsThrough(isSyncStatus(connected: true, hasSynced: false)), + ); return status; } @@ -112,20 +113,19 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': { 'last_op_id': '0', 'buckets': [ - { - 'bucket': 'bkt', - 'checksum': 0, - } + {'bucket': 'bkt', 'checksum': 0}, ], }, }); await expectLater(status, emits(isSyncStatus(downloading: true))); syncService.addLine({ - 'checkpoint_complete': {'last_op_id': '0'} + 'checkpoint_complete': {'last_op_id': '0'}, }); await expectLater( - status, emits(isSyncStatus(downloading: false, hasSynced: true))); + status, + emits(isSyncStatus(downloading: false, hasSynced: true)), + ); await database.disconnect(); final independentDb = TestDatabase( @@ -140,25 +140,31 @@ void _declareTests(String name, SyncOptions options, bool bson) { expect(independentDb.currentStatus.hasSynced, isTrue); // A complete sync also means that all partial syncs have completed expect( - independentDb.currentStatus - .statusForPriority(StreamPriority(3)) - .hasSynced, - isTrue); + independentDb.currentStatus + .statusForPriority(StreamPriority(3)) + .hasSynced, + isTrue, + ); }); // raw tables are only supported by the rust sync client test('raw tables with implicit statements', () async { - final schema = const Schema([], rawTables: [ - RawTable.inferred( - name: 'lists', - schema: RawTableSchema(tableName: 'lists'), - ), - ]); + final schema = const Schema( + [], + rawTables: [ + RawTable.inferred( + name: 'lists', + schema: RawTableSchema(tableName: 'lists'), + ), + ], + ); await database.execute( - 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT);'); + 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT);', + ); final query = StreamQueue( - database.watch('SELECT * FROM lists', throttle: Duration.zero)); + database.watch('SELECT * FROM lists', throttle: Duration.zero), + ); await expectLater(query, emits(isEmpty)); await database.updateSchema(schema); @@ -170,7 +176,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '1', writeCheckpoint: null, checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }) ..addLine({ 'data': { @@ -182,22 +188,19 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'op': 'PUT', 'op_id': '1', 'object_id': 'my_list', - 'object_type': 'lists' - } - ] - } + 'object_type': 'lists', + }, + ], + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '1'} + 'checkpoint_complete': {'last_op_id': '1'}, }); await expectLater( query, emits([ - { - 'id': 'my_list', - 'name': 'custom list', - } + {'id': 'my_list', 'name': 'custom list'}, ]), ); @@ -207,7 +210,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '2', writeCheckpoint: null, checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }) ..addLine({ 'data': { @@ -218,44 +221,47 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'op': 'REMOVE', 'op_id': '2', 'object_id': 'my_list', - 'object_type': 'lists' - } - ] - } + 'object_type': 'lists', + }, + ], + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '2'} + 'checkpoint_complete': {'last_op_id': '2'}, }); await expectLater(query, emits(isEmpty)); }); test('raw tables with explicit statements', () async { - final schema = Schema(const [], rawTables: [ - RawTable( - name: 'lists', - put: PendingStatement( - sql: - 'INSERT OR REPLACE INTO lists (id, name, _rest) VALUES (?, ?, ?)', - params: [ - PendingStatementValue.id(), - PendingStatementValue.column('name'), - PendingStatementValue.rest(), - ], - ), - delete: PendingStatement( - sql: 'DELETE FROM lists WHERE id = ?', - params: [ - PendingStatementValue.id(), - ], + final schema = Schema( + const [], + rawTables: [ + RawTable( + name: 'lists', + put: PendingStatement( + sql: + 'INSERT OR REPLACE INTO lists (id, name, _rest) VALUES (?, ?, ?)', + params: [ + PendingStatementValue.id(), + PendingStatementValue.column('name'), + PendingStatementValue.rest(), + ], + ), + delete: PendingStatement( + sql: 'DELETE FROM lists WHERE id = ?', + params: [PendingStatementValue.id()], + ), ), - ), - ]); + ], + ); await database.execute( - 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT, _rest TEXT);'); + 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT, _rest TEXT);', + ); final query = StreamQueue( - database.watch('SELECT * FROM lists', throttle: Duration.zero)); + database.watch('SELECT * FROM lists', throttle: Duration.zero), + ); await expectLater(query, emits(isEmpty)); await database.updateSchema(schema); @@ -267,7 +273,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '1', writeCheckpoint: null, checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }) ..addLine({ 'data': { @@ -275,18 +281,20 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'data': [ { 'checksum': 0, - 'data': json.encode( - {'name': 'custom list', 'additional_column': 'foo'}), + 'data': json.encode({ + 'name': 'custom list', + 'additional_column': 'foo', + }), 'op': 'PUT', 'op_id': '1', 'object_id': 'my_list', - 'object_type': 'lists' - } - ] - } + 'object_type': 'lists', + }, + ], + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '1'} + 'checkpoint_complete': {'last_op_id': '1'}, }); await expectLater( @@ -295,8 +303,8 @@ void _declareTests(String name, SyncOptions options, bool bson) { { 'id': 'my_list', 'name': 'custom list', - '_rest': json.encode({'additional_column': 'foo'}) - } + '_rest': json.encode({'additional_column': 'foo'}), + }, ]), ); @@ -306,7 +314,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '2', writeCheckpoint: null, checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }) ..addLine({ 'data': { @@ -317,13 +325,13 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'op': 'REMOVE', 'op_id': '2', 'object_id': 'my_list', - 'object_type': 'lists' - } - ] - } + 'object_type': 'lists', + }, + ], + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '2'} + 'checkpoint_complete': {'last_op_id': '2'}, }); await expectLater(query, emits(isEmpty)); @@ -341,14 +349,17 @@ void _declareTests(String name, SyncOptions options, bool bson) { final checksums = [ for (var prio = 0; prio <= 3; prio++) BucketChecksum( - bucket: 'prio$prio', priority: prio, checksum: 10 + prio) + bucket: 'prio$prio', + priority: prio, + checksum: 10 + prio, + ), ]; syncService.addLine({ 'checkpoint': Checkpoint( lastOpId: '4', writeCheckpoint: null, checksums: checksums, - ) + ), }); var operationId = 1; @@ -363,16 +374,18 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'op': 'PUT', 'op_id': '${operationId++}', 'object_id': 'prio$priority', - 'object_type': 'customers' - } - ] - } + 'object_type': 'customers', + }, + ], + }, }); } // Receiving the checkpoint sets the state to downloading await expectLater( - status, emits(isSyncStatus(downloading: true, hasSynced: false))); + status, + emits(isSyncStatus(downloading: true, hasSynced: false)), + ); // Emit partial sync complete for each priority but the last. for (var prio = 0; prio < 3; prio++) { @@ -381,32 +394,37 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'partial_checkpoint_complete': { 'last_op_id': operationId.toString(), 'priority': prio, - } + }, }); await expectLater( status, emitsThrough( - isSyncStatus(downloading: true, hasSynced: false).having( - (e) => e.statusForPriority(StreamPriority(0)).hasSynced, - 'status for $prio', - isTrue, - )), + isSyncStatus(downloading: true, hasSynced: false).having( + (e) => e.statusForPriority(StreamPriority(0)).hasSynced, + 'status for $prio', + isTrue, + ), + ), ); await database.waitForFirstSync(priority: StreamPriority(prio)); - expect(await database.getAll('SELECT * FROM customers'), - hasLength(prio + 1)); + expect( + await database.getAll('SELECT * FROM customers'), + hasLength(prio + 1), + ); } // Complete the sync addRow(3); syncService.addLine({ - 'checkpoint_complete': {'last_op_id': operationId.toString()} + 'checkpoint_complete': {'last_op_id': operationId.toString()}, }); - await expectLater(status, - emitsThrough(isSyncStatus(downloading: false, hasSynced: true))); + await expectLater( + status, + emitsThrough(isSyncStatus(downloading: false, hasSynced: true)), + ); await database.waitForFirstSync(); expect(await database.getAll('SELECT * FROM customers'), hasLength(4)); }); @@ -419,17 +437,14 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '0', writeCheckpoint: null, checksums: [ - BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0) + BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0), ], - ) + ), }); await expectLater(status, emits(isSyncStatus(downloading: true))); syncService.addLine({ - 'partial_checkpoint_complete': { - 'last_op_id': '0', - 'priority': 1, - } + 'partial_checkpoint_complete': {'last_op_id': '0', 'priority': 1}, }); await database.waitForFirstSync(priority: StreamPriority(1)); expect(database.currentStatus.hasSynced, isFalse); @@ -446,15 +461,17 @@ void _declareTests(String name, SyncOptions options, bool bson) { expect(independentDb.currentStatus.hasSynced, isFalse); // Completing a sync for prio 1 implies a completed sync for prio 0 expect( - independentDb.currentStatus - .statusForPriority(StreamPriority(0)) - .hasSynced, - isTrue); + independentDb.currentStatus + .statusForPriority(StreamPriority(0)) + .hasSynced, + isTrue, + ); expect( - independentDb.currentStatus - .statusForPriority(StreamPriority(3)) - .hasSynced, - isFalse); + independentDb.currentStatus + .statusForPriority(StreamPriority(3)) + .hasSynced, + isFalse, + ); }); test( @@ -468,16 +485,14 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '0', writeCheckpoint: null, checksums: [ - BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0) + BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0), ], - ) + ), }); await expectLater(status, emits(isSyncStatus(downloading: true))); syncService.addLine({ - 'checkpoint_complete': { - 'last_op_id': '0', - } + 'checkpoint_complete': {'last_op_id': '0'}, }); await expectLater(status, emits(isSyncStatus(downloading: false))); @@ -523,8 +538,9 @@ void _declareTests(String name, SyncOptions options, bool bson) { // Trigger an upload await database.execute( - 'INSERT INTO customers (id, name, email) VALUES (uuid(), ?, ?)', - ['local', 'local@example.org']); + 'INSERT INTO customers (id, name, email) VALUES (uuid(), ?, ?)', + ['local', 'local@example.org'], + ); await expectCustomerRows(hasLength(1)); await uploadStarted.future; @@ -535,7 +551,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { writeCheckpoint: '1', lastOpId: '2', checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }); await expectLater(status, emitsThrough(isSyncStatus(downloading: true))); @@ -546,12 +562,14 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'data': [ { 'checksum': 0, - 'data': json.encode( - {'name': 'from local', 'email': 'local@example.org'}), + 'data': json.encode({ + 'name': 'from local', + 'email': 'local@example.org', + }), 'op': 'PUT', 'op_id': '1', 'object_id': '1', - 'object_type': 'customers' + 'object_type': 'customers', }, { 'checksum': 0, @@ -559,13 +577,13 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'op': 'PUT', 'op_id': '2', 'object_id': '2', - 'object_type': 'customers' - } - ] - } + 'object_type': 'customers', + }, + ], + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '2'} + 'checkpoint_complete': {'last_op_id': '2'}, }); // Despite receiving a valid checkpoint with two rows, it should not be @@ -578,7 +596,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { syncService.writeCheckpoint = () { sentCheckpoint.complete(); return { - 'data': {'write_checkpoint': '1'} + 'data': {'write_checkpoint': '1'}, }; }; uploadFinished.complete(); @@ -598,7 +616,11 @@ void _declareTests(String name, SyncOptions options, bool bson) { BucketChecksum bucket(String name, int count, {int priority = 3}) { return BucketChecksum( - bucket: name, priority: priority, checksum: 0, count: count); + bucket: name, + priority: priority, + checksum: 0, + count: count, + ); } void addDataLine(String bucket, int amount) { @@ -614,9 +636,9 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'object_id': '$lastOpId', 'checksum': 0, 'data': '{}', - } + }, ], - } + }, }); } @@ -626,13 +648,11 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'partial_checkpoint_complete': { 'last_op_id': '$lastOpId', 'priority': partial, - } + }, }); } else { syncService.addLine({ - 'checkpoint_complete': { - 'last_op_id': '$lastOpId', - } + 'checkpoint_complete': {'last_op_id': '$lastOpId'}, }); } } @@ -644,13 +664,15 @@ void _declareTests(String name, SyncOptions options, bool bson) { }) async { await expectLater( status, - emitsThrough(isSyncStatus( - downloading: true, - downloadProgress: isSyncDownloadProgress( - progress: total, - priorities: priorities, + emitsThrough( + isSyncStatus( + downloading: true, + downloadProgress: isSyncDownloadProgress( + progress: total, + priorities: priorities, + ), ), - )), + ), ); } @@ -660,7 +682,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '10', checksums: [bucket('a', 10)], - ) + ), }); await expectProgress(status, total: progress(0, 10)); @@ -668,8 +690,10 @@ void _declareTests(String name, SyncOptions options, bool bson) { await expectProgress(status, total: progress(10, 10)); addCheckpointComplete(); - await expectLater(status, - emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + await expectLater( + status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull)), + ); // Emit new data, progress should be 0/2 instead of 10/12 syncService.addLine({ @@ -683,8 +707,10 @@ void _declareTests(String name, SyncOptions options, bool bson) { addDataLine('a', 2); await expectProgress(status, total: progress(2, 2)); addCheckpointComplete(); - await expectLater(status, - emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + await expectLater( + status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull)), + ); }); test('interrupted sync', () async { @@ -693,7 +719,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '10', checksums: [bucket('a', 10)], - ) + ), }); await expectProgress(status, total: progress(0, 10)); addDataLine('a', 5); @@ -710,14 +736,16 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '10', checksums: [bucket('a', 10)], - ) + ), }); // Progress should be restored instead of saying e.g 0/5 now. await expectProgress(status, total: progress(5, 10)); addCheckpointComplete(); - await expectLater(status, - emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + await expectLater( + status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull)), + ); }); test('interrupted sync with new checkpoint', () async { @@ -726,7 +754,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '10', checksums: [bucket('a', 10)], - ) + ), }); await expectProgress(status, total: progress(0, 10)); addDataLine('a', 5); @@ -743,13 +771,15 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '12', checksums: [bucket('a', 12)], - ) + ), }); await expectProgress(status, total: progress(5, 12)); addCheckpointComplete(); - await expectLater(status, - emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + await expectLater( + status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull)), + ); }); test('interrupt and defrag', () async { @@ -758,7 +788,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': Checkpoint( lastOpId: '10', checksums: [bucket('a', 10)], - ) + ), }); await expectProgress(status, total: progress(0, 10)); addDataLine('a', 5); @@ -771,10 +801,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { status = await waitForConnection(); syncService.addLine({ - 'checkpoint': Checkpoint( - lastOpId: '14', - checksums: [bucket('a', 4)], - ) + 'checkpoint': Checkpoint(lastOpId: '14', checksums: [bucket('a', 4)]), }); // In this special case, don't report 5/4 as progress @@ -786,10 +813,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { Future checkProgress(Object prio0, Object prio2) async { await expectProgress( status, - priorities: { - StreamPriority(0): prio0, - StreamPriority(2): prio2, - }, + priorities: {StreamPriority(0): prio0, StreamPriority(2): prio2}, total: prio2, ); } @@ -799,7 +823,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '10', checksums: [ bucket('a', 5, priority: 0), - bucket('b', 5, priority: 2) + bucket('b', 5, priority: 2), ], ), }); @@ -820,7 +844,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '14', checksums: [ bucket('a', 8, priority: 0), - bucket('b', 6, priority: 2) + bucket('b', 6, priority: 2), ], ), }); @@ -835,8 +859,10 @@ void _declareTests(String name, SyncOptions options, bool bson) { await checkProgress(progress(8, 8), progress(14, 14)); addCheckpointComplete(); - await expectLater(status, - emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + await expectLater( + status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull)), + ); }); }); @@ -848,7 +874,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '4', writeCheckpoint: null, checksums: [checksum(bucket: 'a', checksum: 0)], - ) + ), }); await expectLater(status, emits(isSyncStatus(downloading: true))); @@ -864,12 +890,12 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '4', writeCheckpoint: null, checksums: [checksum(bucket: 'a', checksum: 10)], - ) + ), }); await expectLater(status, emits(isSyncStatus(downloading: true))); syncService.addLine({ - 'checkpoint_complete': {'last_op_id': '10'} + 'checkpoint_complete': {'last_op_id': '10'}, }); syncService.endCurrentListener(); @@ -885,7 +911,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '4', writeCheckpoint: null, checksums: [checksum(bucket: 'a', checksum: 10)], - ) + ), }); await expectLater(status, emits(isSyncStatus(downloading: true))); @@ -900,8 +926,9 @@ void _declareTests(String name, SyncOptions options, bool bson) { test('uploads writes made while offline', () async { // Local write while not connected await database.execute( - 'insert into customers (id, name) values (uuid(), ?)', - ['local customer']); + 'insert into customers (id, name) values (uuid(), ?)', + ['local customer'], + ); uploadData = (db) async { final batch = await db.getNextCrudTransaction(); if (batch != null) { @@ -909,12 +936,14 @@ void _declareTests(String name, SyncOptions options, bool bson) { } }; syncService.writeCheckpoint = () => { - 'data': {'write_checkpoint': '1'} - }; + 'data': {'write_checkpoint': '1'}, + }; - final query = StreamQueue(database - .watch('SELECT name FROM customers') - .map((e) => e.single['name'])); + final query = StreamQueue( + database + .watch('SELECT name FROM customers') + .map((e) => e.single['name']), + ); expect(await query.next, 'local customer'); await waitForConnection(); @@ -925,7 +954,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { lastOpId: '1', writeCheckpoint: '1', checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], - ) + ), }) ..addLine({ 'data': { @@ -938,12 +967,12 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'object_id': '1', 'checksum': 0, 'data': json.encode({'name': 'from server'}), - } + }, ], - } + }, }) ..addLine({ - 'checkpoint_complete': {'last_op_id': '1'} + 'checkpoint_complete': {'last_op_id': '1'}, }); expect(await query.next, 'from server'); @@ -954,12 +983,15 @@ void _declareTests(String name, SyncOptions options, bool bson) { final requestStarted = Completer(); syncService.router = Router() - ..post('/sync/stream', expectAsync1((Request request) async { - requestStarted.complete(); - - // emulate a network that never connects - await Completer().future; - })); + ..post( + '/sync/stream', + expectAsync1((Request request) async { + requestStarted.complete(); + + // emulate a network that never connects + await Completer().future; + }), + ); await connect(); await requestStarted.future; @@ -975,10 +1007,7 @@ void _declareTests(String name, SyncOptions options, bool bson) { 'checkpoint': { 'last_op_id': '0', 'buckets': [ - { - 'bucket': 'bkt', - 'checksum': 0, - } + {'bucket': 'bkt', 'checksum': 0}, ], }, }); diff --git a/packages/powersync/test/sync/options_test.dart b/packages/powersync/test/sync/options_test.dart index d09594f5..4f15a7db 100644 --- a/packages/powersync/test/sync/options_test.dart +++ b/packages/powersync/test/sync/options_test.dart @@ -4,15 +4,19 @@ import 'package:test/test.dart'; void main() { group('sync options', () { test('can merge with changes', () { - final a = ResolvedSyncOptions(SyncOptions( - params: {'client': 'a'}, - crudThrottleTime: const Duration(seconds: 1), - )); + final a = ResolvedSyncOptions( + SyncOptions( + params: {'client': 'a'}, + crudThrottleTime: const Duration(seconds: 1), + ), + ); - final (b, didChange) = a.applyFrom(SyncOptions( - params: {'client': 'a'}, - retryDelay: const Duration(seconds: 1), - )); + final (b, didChange) = a.applyFrom( + SyncOptions( + params: {'client': 'a'}, + retryDelay: const Duration(seconds: 1), + ), + ); expect(b.params, {'client': 'a'}); expect(b.crudThrottleTime, const Duration(seconds: 1)); @@ -21,15 +25,19 @@ void main() { }); test('can merge without changes', () { - final a = ResolvedSyncOptions(SyncOptions( - params: {'client': 'a'}, - crudThrottleTime: const Duration(seconds: 1), - )); + final a = ResolvedSyncOptions( + SyncOptions( + params: {'client': 'a'}, + crudThrottleTime: const Duration(seconds: 1), + ), + ); - final (_, didChange) = a.applyFrom(SyncOptions( - // This is the default, so no change from a - retryDelay: const Duration(seconds: 5), - )); + final (_, didChange) = a.applyFrom( + SyncOptions( + // This is the default, so no change from a + retryDelay: const Duration(seconds: 5), + ), + ); expect(didChange, isFalse); }); diff --git a/packages/powersync/test/sync/protocol.dart b/packages/powersync/test/sync/protocol.dart index 7d1be213..446baece 100644 --- a/packages/powersync/test/sync/protocol.dart +++ b/packages/powersync/test/sync/protocol.dart @@ -10,13 +10,16 @@ sealed class StreamingSyncLine { return Checkpoint.fromJson(line['checkpoint'] as Map); } else if (line.containsKey('checkpoint_diff')) { return StreamingSyncCheckpointDiff.fromJson( - line['checkpoint_diff'] as Map); + line['checkpoint_diff'] as Map, + ); } else if (line.containsKey('checkpoint_complete')) { return StreamingSyncCheckpointComplete.fromJson( - line['checkpoint_complete'] as Map); + line['checkpoint_complete'] as Map, + ); } else if (line.containsKey('partial_checkpoint_complete')) { return StreamingSyncCheckpointPartiallyComplete.fromJson( - line['partial_checkpoint_complete'] as Map); + line['partial_checkpoint_complete'] as Map, + ); } else if (line.containsKey('data')) { return SyncDataBatch([ SyncBucketData.fromJson(line['data'] as Map), @@ -45,15 +48,18 @@ final class Checkpoint extends StreamingSyncLine { final String? writeCheckpoint; final List checksums; - const Checkpoint( - {required this.lastOpId, required this.checksums, this.writeCheckpoint}); + const Checkpoint({ + required this.lastOpId, + required this.checksums, + this.writeCheckpoint, + }); Checkpoint.fromJson(Map json) - : lastOpId = json['last_op_id'] as String, - writeCheckpoint = json['write_checkpoint'] as String?, - checksums = (json['buckets'] as List) - .map((b) => BucketChecksum.fromJson(b as Map)) - .toList(); + : lastOpId = json['last_op_id'] as String, + writeCheckpoint = json['write_checkpoint'] as String?, + checksums = (json['buckets'] as List) + .map((b) => BucketChecksum.fromJson(b as Map)) + .toList(); Map toJson({int? priority}) { return { @@ -62,7 +68,7 @@ final class Checkpoint extends StreamingSyncLine { 'buckets': checksums .where((c) => priority == null || c.priority <= priority) .map((c) => c.toJson()) - .toList(growable: false) + .toList(growable: false), }; } } @@ -76,22 +82,23 @@ class BucketChecksum { final int? count; final String? lastOpId; - const BucketChecksum( - {required this.bucket, - required this.priority, - required this.checksum, - this.count, - this.lastOpId}); + const BucketChecksum({ + required this.bucket, + required this.priority, + required this.checksum, + this.count, + this.lastOpId, + }); BucketChecksum.fromJson(Map json) - : bucket = json['bucket'] as String, - // Use the default priority (3) as a fallback if the server doesn't send - // priorities. This value is arbitrary though, it won't get used since - // servers not sending priorities also won't send partial checkpoints. - priority = json['priority'] as int? ?? 3, - checksum = json['checksum'] as int, - count = json['count'] as int?, - lastOpId = json['last_op_id'] as String?; + : bucket = json['bucket'] as String, + // Use the default priority (3) as a fallback if the server doesn't send + // priorities. This value is arbitrary though, it won't get used since + // servers not sending priorities also won't send partial checkpoints. + priority = json['priority'] as int? ?? 3, + checksum = json['checksum'] as int, + count = json['count'] as int?, + lastOpId = json['last_op_id'] as String?; Map toJson() { return { @@ -115,15 +122,18 @@ final class StreamingSyncCheckpointDiff extends StreamingSyncLine { String? writeCheckpoint; StreamingSyncCheckpointDiff( - this.lastOpId, this.updatedBuckets, this.removedBuckets); + this.lastOpId, + this.updatedBuckets, + this.removedBuckets, + ); StreamingSyncCheckpointDiff.fromJson(Map json) - : lastOpId = json['last_op_id'] as String, - writeCheckpoint = json['write_checkpoint'] as String?, - updatedBuckets = (json['updated_buckets'] as List) - .map((e) => BucketChecksum.fromJson(e as Map)) - .toList(), - removedBuckets = (json['removed_buckets'] as List).cast(); + : lastOpId = json['last_op_id'] as String, + writeCheckpoint = json['write_checkpoint'] as String?, + updatedBuckets = (json['updated_buckets'] as List) + .map((e) => BucketChecksum.fromJson(e as Map)) + .toList(), + removedBuckets = (json['removed_buckets'] as List).cast(); } /// Sent after the last [SyncBucketData] message for a checkpoint. @@ -137,7 +147,7 @@ final class StreamingSyncCheckpointComplete extends StreamingSyncLine { StreamingSyncCheckpointComplete(this.lastOpId); StreamingSyncCheckpointComplete.fromJson(Map json) - : lastOpId = json['last_op_id'] as String; + : lastOpId = json['last_op_id'] as String; } /// Sent after all the [SyncBucketData] messages for a given priority within a @@ -149,8 +159,8 @@ final class StreamingSyncCheckpointPartiallyComplete extends StreamingSyncLine { StreamingSyncCheckpointPartiallyComplete(this.lastOpId, this.bucketPriority); StreamingSyncCheckpointPartiallyComplete.fromJson(Map json) - : lastOpId = json['last_op_id'] as String, - bucketPriority = json['priority'] as int; + : lastOpId = json['last_op_id'] as String, + bucketPriority = json['priority'] as int; } /// Sent as a periodic ping to keep the connection alive and to notify the @@ -164,7 +174,7 @@ final class StreamingSyncKeepalive extends StreamingSyncLine { StreamingSyncKeepalive(this.tokenExpiresIn); StreamingSyncKeepalive.fromJson(Map json) - : tokenExpiresIn = json['token_expires_in'] as int; + : tokenExpiresIn = json['token_expires_in'] as int; } class StreamingSyncRequest { @@ -175,7 +185,11 @@ class StreamingSyncRequest { Map? appMetadata; StreamingSyncRequest( - this.buckets, this.parameters, this.clientId, this.appMetadata); + this.buckets, + this.parameters, + this.clientId, + this.appMetadata, + ); Map toJson() { final Map json = { @@ -200,10 +214,7 @@ class BucketRequest { BucketRequest(this.name, this.after); - Map toJson() => { - 'name': name, - 'after': after, - }; + Map toJson() => {'name': name, 'after': after}; } /// A batch of sync operations being delivered from the sync service. @@ -227,21 +238,22 @@ final class SyncBucketData { final String? after; final String? nextAfter; - const SyncBucketData( - {required this.bucket, - required this.data, - this.hasMore = false, - this.after, - this.nextAfter}); + const SyncBucketData({ + required this.bucket, + required this.data, + this.hasMore = false, + this.after, + this.nextAfter, + }); SyncBucketData.fromJson(Map json) - : bucket = json['bucket'] as String, - hasMore = json['has_more'] as bool? ?? false, - after = json['after'] as String?, - nextAfter = json['next_after'] as String?, - data = (json['data'] as List) - .map((e) => OplogEntry.fromJson(e as Map)) - .toList(); + : bucket = json['bucket'] as String, + hasMore = json['has_more'] as bool? ?? false, + after = json['after'] as String?, + nextAfter = json['next_after'] as String?, + data = (json['data'] as List) + .map((e) => OplogEntry.fromJson(e as Map)) + .toList(); Map toJson() { return { @@ -249,7 +261,7 @@ final class SyncBucketData { 'has_more': hasMore, 'after': after, 'next_after': nextAfter, - 'data': data + 'data': data, }; } } @@ -271,29 +283,30 @@ class OplogEntry { final String? data; final int checksum; - const OplogEntry( - {required this.opId, - required this.op, - this.subkey, - this.rowType, - this.rowId, - this.data, - required this.checksum}); + const OplogEntry({ + required this.opId, + required this.op, + this.subkey, + this.rowType, + this.rowId, + this.data, + required this.checksum, + }); OplogEntry.fromJson(Map json) - : opId = json['op_id'] as String, - op = OpType.fromJson(json['op'] as String), - rowType = json['object_type'] as String?, - rowId = json['object_id'] as String?, - checksum = json['checksum'] as int, - data = switch (json['data']) { - String data => data, - var other => jsonEncode(other), - }, - subkey = switch (json['subkey']) { - String subkey => subkey, - _ => null, - }; + : opId = json['op_id'] as String, + op = OpType.fromJson(json['op'] as String), + rowType = json['object_type'] as String?, + rowId = json['object_id'] as String?, + checksum = json['checksum'] as int, + data = switch (json['data']) { + String data => data, + var other => jsonEncode(other), + }, + subkey = switch (json['subkey']) { + String subkey => subkey, + _ => null, + }; Map? get parsedData { return switch (data) { @@ -317,7 +330,7 @@ class OplogEntry { 'object_id': rowId, 'checksum': checksum, 'subkey': subkey, - 'data': data + 'data': data, }; } } diff --git a/packages/powersync/test/sync/stream_test.dart b/packages/powersync/test/sync/stream_test.dart index 6e33fa77..4ad55a0e 100644 --- a/packages/powersync/test/sync/stream_test.dart +++ b/packages/powersync/test/sync/stream_test.dart @@ -28,17 +28,14 @@ void main() { database.httpClient = client; await database.connect( - connector: TestConnector( - () async { - credentialsCallbackCount++; - return PowerSyncCredentials( - endpoint: server.url.toString(), - token: 'token$credentialsCallbackCount', - expiresAt: DateTime.now(), - ); - }, - uploadData: (db) async {}, - ), + connector: TestConnector(() async { + credentialsCallbackCount++; + return PowerSyncCredentials( + endpoint: server.url.toString(), + token: 'token$credentialsCallbackCount', + expiresAt: DateTime.now(), + ); + }, uploadData: (db) async {}), options: options, ); } @@ -63,8 +60,9 @@ void main() { await syncService.stop(); }); - Future> waitForConnection( - {bool expectNoWarnings = true}) async { + Future> waitForConnection({ + bool expectNoWarnings = true, + }) async { if (expectNoWarnings) { logger.onRecord.listen((e) { if (e.level >= Level.WARNING) { @@ -82,7 +80,9 @@ void main() { syncService.addKeepAlive(); await expectLater( - status, emitsThrough(isSyncStatus(connected: true, hasSynced: false))); + status, + emitsThrough(isSyncStatus(connected: true, hasSynced: false)), + ); return status; } @@ -94,14 +94,17 @@ void main() { await waitForConnection(); final request = await syncService.waitForListener; - expect(json.decode(await request.readAsString()), - containsPair('streams', containsPair('include_defaults', false))); + expect( + json.decode(await request.readAsString()), + containsPair('streams', containsPair('include_defaults', false)), + ); }); test('subscribes with streams', () async { final a = await database.syncStream('stream', {'foo': 'a'}).subscribe(); - final b = await database.syncStream('stream', {'foo': 'b'}).subscribe( - priority: StreamPriority(1)); + final b = await database + .syncStream('stream', {'foo': 'b'}) + .subscribe(priority: StreamPriority(1)); final statusStream = await waitForConnection(); final request = await syncService.waitForListener; @@ -128,16 +131,21 @@ void main() { checkpoint( lastOpId: 0, buckets: [ - bucketDescription('a', subscriptions: [ - {'sub': 0} - ]), - bucketDescription('b', priority: 1, subscriptions: [ - {'sub': 1} - ]) - ], - streams: [ - stream('stream', false), + bucketDescription( + 'a', + subscriptions: [ + {'sub': 0}, + ], + ), + bucketDescription( + 'b', + priority: 1, + subscriptions: [ + {'sub': 1}, + ], + ), ], + streams: [stream('stream', false)], ), ); @@ -206,18 +214,16 @@ void main() { await didStop.future; syncService.endCurrentListener(); final request = await syncService.waitForListener; - expect(logLines, - contains('Ending Rust sync iteration. Immediate restart: true')); + expect( + logLines, + contains('Ending Rust sync iteration. Immediate restart: true'), + ); expect( json.decode(await request.readAsString()), containsPair( 'streams', containsPair('subscriptions', [ - { - 'stream': 'a', - 'parameters': null, - 'override_priority': null, - }, + {'stream': 'a', 'parameters': null, 'override_priority': null}, ]), ), ); @@ -247,16 +253,14 @@ void main() { // the core extension extends the lifetime of streams currently referenced // before connecting. await database.execute( - 'UPDATE ps_stream_subscriptions SET expires_at = unixepoch() - 1000'); + 'UPDATE ps_stream_subscriptions SET expires_at = unixepoch() - 1000', + ); await waitForConnection(); final request = await syncService.waitForListener; expect( json.decode(await request.readAsString()), - containsPair( - 'streams', - containsPair('subscriptions', isNotEmpty), - ), + containsPair('streams', containsPair('subscriptions', isNotEmpty)), ); aAgain.unsubscribe(); }); @@ -270,10 +274,7 @@ void main() { final request = await syncService.waitForListener; expect( json.decode(await request.readAsString()), - containsPair( - 'streams', - containsPair('subscriptions', isEmpty), - ), + containsPair('streams', containsPair('subscriptions', isEmpty)), ); a.unsubscribe(); }); @@ -289,10 +290,7 @@ void main() { final request = await syncService.waitForListener; expect( json.decode(await request.readAsString()), - containsPair( - 'app_metadata', - containsPair('foo', 'bar'), - ), + containsPair('app_metadata', containsPair('foo', 'bar')), ); }); } diff --git a/packages/powersync/test/sync/streaming_sync_test.dart b/packages/powersync/test/sync/streaming_sync_test.dart index 44607c36..4b4eddb9 100644 --- a/packages/powersync/test/sync/streaming_sync_test.dart +++ b/packages/powersync/test/sync/streaming_sync_test.dart @@ -35,8 +35,10 @@ void main() { final server = await createServer(); final ignoreLogger = Logger.detached('powersync.test'); - final pdb = - await testUtils.setupPowerSync(path: path, logger: ignoreLogger); + final pdb = await testUtils.setupPowerSync( + path: path, + logger: ignoreLogger, + ); const options = SyncOptions(retryDelay: Duration(seconds: 5)); final connector = TestConnector(() async { return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); @@ -68,12 +70,19 @@ void main() { final server = await createServer(mockSyncService: service); final ignoreLogger = Logger.detached('powersync.test'); - final pdb = - await testUtils.setupPowerSync(path: path, logger: ignoreLogger); + final pdb = await testUtils.setupPowerSync( + path: path, + logger: ignoreLogger, + ); const options = SyncOptions(retryDelay: Duration(seconds: 5)); - final connector = TestConnector(expectAsync0(() async { - return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); - })); + final connector = TestConnector( + expectAsync0(() async { + return PowerSyncCredentials( + endpoint: server.endpoint, + token: 'token', + ); + }), + ); await pdb.connect(connector: connector, options: options); while (server.connectionCount != 1) { @@ -107,13 +116,18 @@ void main() { final ignoreLogger = Logger.detached('powersync.test'); final pdb = await testUtils.setupPowerSync( - path: path, logger: ignoreLogger, initialize: false); + path: path, + logger: ignoreLogger, + initialize: false, + ); const options = SyncOptions(retryDelay: Duration(seconds: 5)); await pdb.connect( connector: TestConnector(() async { return PowerSyncCredentials( - endpoint: server.endpoint, token: 'token'); + endpoint: server.endpoint, + token: 'token', + ); }), options: options, ); @@ -121,7 +135,8 @@ void main() { await expectLater( pdb.statusStream, emitsThrough( - isA().having((e) => e.connected, 'connected', isTrue)), + isA().having((e) => e.connected, 'connected', isTrue), + ), ); }); @@ -135,7 +150,9 @@ void main() { credentialsCallback() async { return PowerSyncCredentials( - endpoint: server.endpoint, token: 'token'); + endpoint: server.endpoint, + token: 'token', + ); } final pdb = await testUtils.setupPowerSync(path: path); @@ -154,7 +171,8 @@ void main() { final watch = Stopwatch()..start(); while (server.connectionCount != 0 && watch.elapsedMilliseconds < 100) { await Future.delayed( - Duration(milliseconds: random.nextInt(10))); + Duration(milliseconds: random.nextInt(10)), + ); } expect(server.connectionCount, equals(0)); @@ -249,9 +267,15 @@ void main() { final statusChanges = StreamQueue(pdb.statusStream); await pdb.connect(connector: connector); await expectLater( - statusChanges, - emitsThrough(isA() - .having((e) => e.downloadError, 'downloadError', isNotNull))); + statusChanges, + emitsThrough( + isA().having( + (e) => e.downloadError, + 'downloadError', + isNotNull, + ), + ), + ); await statusChanges.cancel(); expect(pdb.currentStatus.downloadError, exception); diff --git a/packages/powersync/test/sync/sync_status_test.dart b/packages/powersync/test/sync/sync_status_test.dart index 963cf39b..19d7e054 100644 --- a/packages/powersync/test/sync/sync_status_test.dart +++ b/packages/powersync/test/sync/sync_status_test.dart @@ -5,52 +5,57 @@ import 'package:test/test.dart'; void main() { group('SyncStatus.toString', () { test('default', () { - expect(SyncStatus.uninitialized().toString(), - 'SyncStatus'); + expect( + SyncStatus.uninitialized().toString(), + 'SyncStatus', + ); }); test('connection status', () { expect( - (MutableSyncStatus()..connected = true) - .immutableSnapshot() - .toString(), - contains('SyncStatus isSyncStatus({ } if (downloadProgress != null) { matcher = matcher.having( - (e) => e.downloadProgress, 'downloadProgress', downloadProgress); + (e) => e.downloadProgress, + 'downloadProgress', + downloadProgress, + ); } if (syncStreams != null) { matcher = matcher.having((e) => e.syncStreams, 'syncStreams', syncStreams); @@ -39,11 +42,17 @@ TypeMatcher isSyncDownloadProgress({ required Object progress, Map priorities = const {}, }) { - var matcher = - isA().having((e) => e, 'untilCompletion', progress); + var matcher = isA().having( + (e) => e, + 'untilCompletion', + progress, + ); priorities.forEach((priority, expected) { matcher = matcher.having( - (e) => e.untilPriority(priority), 'untilPriority($priority)', expected); + (e) => e.untilPriority(priority), + 'untilPriority($priority)', + expected, + ); }); return matcher; @@ -59,8 +68,11 @@ TypeMatcher isStreamStatus({ required Object? subscription, Object? progress, }) { - var matcher = isA() - .having((e) => e.subscription, 'subscription', subscription); + var matcher = isA().having( + (e) => e.subscription, + 'subscription', + subscription, + ); if (progress case final progress?) { matcher = matcher.having((e) => e.progress, 'progress', progress); } @@ -84,8 +96,11 @@ TypeMatcher isSyncSubscription({ return matcher; } -BucketChecksum checksum( - {required String bucket, required int checksum, int priority = 1}) { +BucketChecksum checksum({ + required String bucket, + required int checksum, + int priority = 1, +}) { return BucketChecksum(bucket: bucket, priority: priority, checksum: checksum); } @@ -102,7 +117,7 @@ Object checkpoint({ 'write_checkpoint': null, 'buckets': buckets, 'streams': streams, - } + }, }; } diff --git a/packages/powersync/test/test_server.dart b/packages/powersync/test/test_server.dart index f1f38f5c..3b8eb0d2 100644 --- a/packages/powersync/test/test_server.dart +++ b/packages/powersync/test/test_server.dart @@ -22,7 +22,10 @@ final class TestServer { app.post('/sync/stream', handleSyncStream); // Open on an arbitrary open port server = await shelf_io.serve( - mockSyncService?.router.call ?? app.call, 'localhost', 0); + mockSyncService?.router.call ?? app.call, + 'localhost', + 0, + ); } String get endpoint { @@ -52,12 +55,8 @@ final class TestServer { return Response.ok( encodeNdjson(stream()), - headers: { - 'Content-Type': 'application/x-ndjson', - }, - context: { - 'shelf.io.buffer_output': false, - }, + headers: {'Content-Type': 'application/x-ndjson'}, + context: {'shelf.io.buffer_output': false}, ); } diff --git a/packages/powersync/test/upload_test.dart b/packages/powersync/test/upload_test.dart index 56980e35..7dc6c939 100644 --- a/packages/powersync/test/upload_test.dart +++ b/packages/powersync/test/upload_test.dart @@ -45,30 +45,38 @@ void main() { } final records = []; - final sub = - testWarningLogger.onRecord.listen((log) => records.add(log.message)); - - powersync = - await testUtils.setupPowerSync(path: path, logger: testWarningLogger); + final sub = testWarningLogger.onRecord.listen( + (log) => records.add(log.message), + ); + + powersync = await testUtils.setupPowerSync( + path: path, + logger: testWarningLogger, + ); // Use a short retry delay here. // A zero retry delay makes this test unstable, since it expects `2` error logs later. // ignore: deprecated_member_use_from_same_package powersync.retryDelay = Duration(milliseconds: 100); - var connector = - TestConnector(credentialsCallback, uploadData: uploadData); + var connector = TestConnector( + credentialsCallback, + uploadData: uploadData, + ); powersync.connect(connector: connector); // Create something with CRUD in it. await powersync.execute( - 'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']); + 'INSERT INTO assets(id, description) VALUES(?, ?)', + [testId, 'test'], + ); // Wait for the uploadData to be called. await Future.delayed(Duration(milliseconds: 100)); // Create something else with CRUD in it. await powersync.execute( - 'INSERT INTO assets(id, description) VALUES(?, ?)', - [testId2, 'test2']); + 'INSERT INTO assets(id, description) VALUES(?, ?)', + [testId2, 'test2'], + ); sub.cancel(); diff --git a/packages/powersync/test/utils/abstract_test_utils.dart b/packages/powersync/test/utils/abstract_test_utils.dart index 1c1096df..983c369f 100644 --- a/packages/powersync/test/utils/abstract_test_utils.dart +++ b/packages/powersync/test/utils/abstract_test_utils.dart @@ -16,19 +16,23 @@ import 'package:test/test.dart'; import 'package:test_api/src/backend/invoker.dart'; const schema = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.text('customer_id'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), - Table('customers', [Column.text('name'), Column.text('email')]) + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.text('customer_id'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]), + ], + ), + Table('customers', [Column.text('name'), Column.text('email')]), ]); const defaultSchema = schema; @@ -42,7 +46,8 @@ Logger _makeTestLogger({Level level = Level.ALL, String? name}) { logger.level = level; logger.onRecord.listen((record) { print( - '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}', + ); if (record.error != null) { print(record.error); } @@ -67,8 +72,9 @@ abstract class AbstractTestUtils { String get _testName => Invoker.current!.liveTest.test.name; String dbPath() { - var testShortName = - _testName.replaceAll(RegExp(r'[\s\./]'), '_').toLowerCase(); + var testShortName = _testName + .replaceAll(RegExp(r'[\s\./]'), '_') + .toLowerCase(); var dbName = "test-db/$testShortName.db"; return dbName; } @@ -89,9 +95,10 @@ abstract class AbstractTestUtils { bool initialize = true, }) async { final db = PowerSyncDatabase.withFactory( - await testFactory(path: path, encryption: encryption), - schema: schema ?? defaultSchema, - logger: logger ?? _makeTestLogger(name: _testName)); + await testFactory(path: path, encryption: encryption), + schema: schema ?? defaultSchema, + logger: logger ?? _makeTestLogger(name: _testName), + ); if (initialize) { await db.initialize(); } @@ -119,7 +126,8 @@ abstract class AbstractTestUtils { }) { return TestDatabase( database: SqliteDatabase.singleConnection( - SqliteConnection.synchronousWrapper(raw)), + SqliteConnection.synchronousWrapper(raw), + ), logger: logger ?? Logger.detached('PowerSync.test'), schema: customSchema ?? schema, ); @@ -130,9 +138,10 @@ class TestConnector extends PowerSyncBackendConnector { Future Function() fetchCredentialsCallback; Future Function(PowerSyncDatabase)? uploadDataCallback; - TestConnector(this.fetchCredentialsCallback, - {Future Function(PowerSyncDatabase)? uploadData}) - : uploadDataCallback = uploadData; + TestConnector( + this.fetchCredentialsCallback, { + Future Function(PowerSyncDatabase)? uploadData, + }) : uploadDataCallback = uploadData; @override Future fetchCredentials() { @@ -178,8 +187,9 @@ final class TestDatabase extends BasePowerSyncDatabase { options: options, connector: InternalConnector.wrap(connector, this), logger: logger, - crudUpdateTriggerStream: database - .onChange(['ps_crud'], throttle: const Duration(milliseconds: 10)), + crudUpdateTriggerStream: database.onChange([ + 'ps_crud', + ], throttle: const Duration(milliseconds: 10)), activeSubscriptions: initiallyActiveStreams, ); impl.statusStream.listen(setStatus); @@ -195,18 +205,30 @@ final class TestDatabase extends BasePowerSyncDatabase { } @override - Future readLock(Future Function(SqliteReadContext tx) callback, - {String? debugContext, Duration? lockTimeout}) async { + Future readLock( + Future Function(SqliteReadContext tx) callback, { + String? debugContext, + Duration? lockTimeout, + }) async { await isInitialized; - return database.readLock(callback, - debugContext: debugContext, lockTimeout: lockTimeout); + return database.readLock( + callback, + debugContext: debugContext, + lockTimeout: lockTimeout, + ); } @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {String? debugContext, Duration? lockTimeout}) async { + Future writeLock( + Future Function(SqliteWriteContext tx) callback, { + String? debugContext, + Duration? lockTimeout, + }) async { await isInitialized; - return database.writeLock(callback, - debugContext: debugContext, lockTimeout: lockTimeout); + return database.writeLock( + callback, + debugContext: debugContext, + lockTimeout: lockTimeout, + ); } } diff --git a/packages/powersync/test/utils/in_memory_http.dart b/packages/powersync/test/utils/in_memory_http.dart index a35d6a09..365ab30f 100644 --- a/packages/powersync/test/utils/in_memory_http.dart +++ b/packages/powersync/test/utils/in_memory_http.dart @@ -34,7 +34,9 @@ final class _MockServer implements shelf.Server { Uri get url => mockHttpUri; Future handleRequest( - BaseRequest request, ByteStream body) async { + BaseRequest request, + ByteStream body, + ) async { final cancellationFuture = switch (request) { Abortable(:final abortTrigger) => abortTrigger, _ => null, @@ -73,29 +75,26 @@ extension on Stream { return this; } - return Stream.multi( - (listener) { - final subscription = listen( - listener.addSync, - onError: listener.addErrorSync, - onDone: listener.closeSync, - ); - - listener - ..onPause = subscription.pause - ..onResume = subscription.resume - ..onCancel = subscription.cancel; - - token.whenComplete(() { - if (!listener.isClosed) { - listener - ..addErrorSync(RequestAbortedException()) - ..closeSync(); - subscription.cancel(); - } - }); - }, - isBroadcast: isBroadcast, - ); + return Stream.multi((listener) { + final subscription = listen( + listener.addSync, + onError: listener.addErrorSync, + onDone: listener.closeSync, + ); + + listener + ..onPause = subscription.pause + ..onResume = subscription.resume + ..onCancel = subscription.cancel; + + token.whenComplete(() { + if (!listener.isClosed) { + listener + ..addErrorSync(RequestAbortedException()) + ..closeSync(); + subscription.cancel(); + } + }); + }, isBroadcast: isBroadcast); } } diff --git a/packages/powersync/test/utils/stub_test_utils.dart b/packages/powersync/test/utils/stub_test_utils.dart index 38afdb0b..7703b42f 100644 --- a/packages/powersync/test/utils/stub_test_utils.dart +++ b/packages/powersync/test/utils/stub_test_utils.dart @@ -6,11 +6,12 @@ import 'abstract_test_utils.dart'; class TestUtils extends AbstractTestUtils { @override - Future testFactory( - {String? path, - String sqlitePath = '', - SqliteOptions options = const SqliteOptions(), - EncryptionOptions? encryption}) { + Future testFactory({ + String? path, + String sqlitePath = '', + SqliteOptions options = const SqliteOptions(), + EncryptionOptions? encryption, + }) { throw UnimplementedError(); } diff --git a/packages/powersync/test/utils/web_test_utils.dart b/packages/powersync/test/utils/web_test_utils.dart index 5dc6faaa..f905851a 100644 --- a/packages/powersync/test/utils/web_test_utils.dart +++ b/packages/powersync/test/utils/web_test_utils.dart @@ -23,8 +23,10 @@ class TestUtils extends AbstractTestUtils { } Future _init() async { - final channel = - spawnHybridUri('/test/server/worker_server.dart', stayAlive: true); + final channel = spawnHybridUri( + '/test/server/worker_server.dart', + stayAlive: true, + ); final port = await channel.stream.first as int; sqlite3WASMUri = 'http://localhost:$port/sqlite3.wasm'; sqlite3McUri = 'http://localhost:$port/sqlite3mc.wasm'; @@ -32,8 +34,9 @@ class TestUtils extends AbstractTestUtils { final workerUriSource = 'http://localhost:$port/powersync_db.worker.js'; final blob = Blob( - ['importScripts("$workerUriSource");'.toJS].toJS, - BlobPropertyBag(type: 'application/javascript')); + ['importScripts("$workerUriSource");'.toJS].toJS, + BlobPropertyBag(type: 'application/javascript'), + ); workerUri = _createObjectURL(blob); } diff --git a/packages/powersync/test/version_test.dart b/packages/powersync/test/version_test.dart index 06f9d871..c5527a62 100644 --- a/packages/powersync/test/version_test.dart +++ b/packages/powersync/test/version_test.dart @@ -12,8 +12,11 @@ void main() { final pubspec = loadYamlDocument(File('pubspec.yaml').readAsStringSync()); final versionInPubspec = (pubspec.contents as YamlMap)['version'] as String; - expect(libraryVersion, versionInPubspec, - reason: - 'Version in lib/src/version.dart ($libraryVersion) must match version in pubspec ($versionInPubspec)'); + expect( + libraryVersion, + versionInPubspec, + reason: + 'Version in lib/src/version.dart ($libraryVersion) must match version in pubspec ($versionInPubspec)', + ); }); } diff --git a/packages/powersync/test/watch_test.dart b/packages/powersync/test/watch_test.dart index d51dbeb1..c0f241a5 100644 --- a/packages/powersync/test/watch_test.dart +++ b/packages/powersync/test/watch_test.dart @@ -11,18 +11,22 @@ import 'utils/test_utils_impl.dart'; final testUtils = TestUtils(); const testSchema = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.text('customer_id'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), + Table( + 'assets', + [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.text('customer_id'), + Column.text('description'), + ], + indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]), + ], + ), Table('customers', [Column.text('name'), Column.text('email')]), Table('other_customers', [Column.text('name'), Column.text('email')]), ]); @@ -37,29 +41,36 @@ void main() { }); test('watch', () async { - final powersync = - await testUtils.setupPowerSync(path: path, schema: testSchema); + final powersync = await testUtils.setupPowerSync( + path: path, + schema: testSchema, + ); const baseTime = 20; const throttleDuration = Duration(milliseconds: baseTime); final stream = powersync.watch( - 'SELECT count() AS count FROM assets INNER JOIN customers ON customers.id = assets.customer_id', - throttle: throttleDuration); + 'SELECT count() AS count FROM assets INNER JOIN customers ON customers.id = assets.customer_id', + throttle: throttleDuration, + ); var id = uuid.v4(); - await powersync.execute( - 'INSERT INTO customers(id, name) VALUES (?, ?)', [id, 'a customer']); + await powersync.execute('INSERT INTO customers(id, name) VALUES (?, ?)', [ + id, + 'a customer', + ]); var done = false; Future inserts() async { while (!done) { await powersync.execute( - 'INSERT INTO assets(id, make, customer_id) VALUES (uuid(), ?, ?)', - ['test', id]); + 'INSERT INTO assets(id, make, customer_id) VALUES (uuid(), ?, ?)', + ['test', id], + ); await Future.delayed( - Duration(milliseconds: Random().nextInt(baseTime * 2))); + Duration(milliseconds: Random().nextInt(baseTime * 2)), + ); } } @@ -86,8 +97,10 @@ void main() { // The number of read queries must not be greater than the number of //writes overall, plus one for an initial read. - expect(numberOfQueries, - lessThanOrEqualTo((results.last.first['count'] as int) + 1)); + expect( + numberOfQueries, + lessThanOrEqualTo((results.last.first['count'] as int) + 1), + ); DateTime? lastTime; for (var r in times) { @@ -105,8 +118,10 @@ void main() { }); test('onChange', () async { - final powersync = - await testUtils.setupPowerSync(path: path, schema: testSchema); + final powersync = await testUtils.setupPowerSync( + path: path, + schema: testSchema, + ); const baseTime = 20; @@ -116,30 +131,35 @@ void main() { Future inserts() async { while (!done) { await powersync.execute( - 'INSERT INTO assets(id, make) VALUES (uuid(), ?)', ['test']); + 'INSERT INTO assets(id, make) VALUES (uuid(), ?)', + ['test'], + ); await Future.delayed( - Duration(milliseconds: Random().nextInt(baseTime))); + Duration(milliseconds: Random().nextInt(baseTime)), + ); } } final insertsFuture = inserts(); - final stream = powersync.onChange({'assets', 'customers'}, - throttle: throttleDuration).asyncMap((event) async { - // This is where queries would typically be executed - return event; - }); + final stream = powersync + .onChange({'assets', 'customers'}, throttle: throttleDuration) + .asyncMap((event) async { + // This is where queries would typically be executed + return event; + }); var events = await stream.take(3).toList(); done = true; expect( - events, - equals([ - UpdateNotification.empty(), - UpdateNotification.single('assets'), - UpdateNotification.single('assets') - ])); + events, + equals([ + UpdateNotification.empty(), + UpdateNotification.single('assets'), + UpdateNotification.single('assets'), + ]), + ); await insertsFuture; }); @@ -147,22 +167,22 @@ void main() { final powersync = await testUtils.setupPowerSync( path: path, schema: Schema([ - Table.localOnly('users', [ - Column.text('name'), - ]), - Table('assets', [ - Column.text('name'), - ]), + Table.localOnly('users', [Column.text('name')]), + Table('assets', [Column.text('name')]), ]), ); final updates = StreamQueue(powersync.updates); - await powersync - .execute('INSERT INTO users (id, name) VALUES (uuid(), ?)', ['test']); + await powersync.execute( + 'INSERT INTO users (id, name) VALUES (uuid(), ?)', + ['test'], + ); await expectLater(updates, emits(UpdateNotification({'users'}))); await powersync.execute( - 'INSERT INTO assets (id, name) VALUES (uuid(), ?)', ['test']); + 'INSERT INTO assets (id, name) VALUES (uuid(), ?)', + ['test'], + ); await expectLater(updates, emits(UpdateNotification({'assets'}))); }); }); diff --git a/packages/powersync/test/web/http_test.dart b/packages/powersync/test/web/http_test.dart index 4f0160a2..ef14f3c8 100644 --- a/packages/powersync/test/web/http_test.dart +++ b/packages/powersync/test/web/http_test.dart @@ -16,17 +16,22 @@ void main() { final uri = Uri.parse('https://powersync.com/foo/bar'); test('can send http requests', () async { - final (client, _) = await createRemoteClient(MockClient((request) async { - expect(request.url, uri); - expect(request.method, 'POST'); - expect(request.headers, containsPair('Foo', 'Bar')); - expect(request.body, 'body'); - - return Response('ok', 200, headers: {'Response': 'Ok'}); - })); - - final response = - await client.post(uri, headers: {'Foo': 'Bar'}, body: 'body'); + final (client, _) = await createRemoteClient( + MockClient((request) async { + expect(request.url, uri); + expect(request.method, 'POST'); + expect(request.headers, containsPair('Foo', 'Bar')); + expect(request.body, 'body'); + + return Response('ok', 200, headers: {'Response': 'Ok'}); + }), + ); + + final response = await client.post( + uri, + headers: {'Foo': 'Bar'}, + body: 'body', + ); expect(response.statusCode, 200); expect(response.headers, {'Response': 'Ok'}); expect(response.body, 'ok'); @@ -34,11 +39,12 @@ void main() { test('response stream control', () async { final responseStream = StreamController(); - final (client, _) = - await createRemoteClient(MockClient.streaming((request, stream) async { - await stream.drain(); - return StreamedResponse(responseStream.stream, 200); - })); + final (client, _) = await createRemoteClient( + MockClient.streaming((request, stream) async { + await stream.drain(); + return StreamedResponse(responseStream.stream, 200); + }), + ); final response = await client.send(Request('GET', uri)); expect(responseStream.hasListener, isFalse); @@ -69,11 +75,12 @@ void main() { group('can abort', () { test('before receiving response', () async { - final (client, _) = - await createRemoteClient(MockClient.streaming((request, _) async { - await (request as Abortable).abortTrigger!; - throw RequestAbortedException(); - })); + final (client, _) = await createRemoteClient( + MockClient.streaming((request, _) async { + await (request as Abortable).abortTrigger!; + throw RequestAbortedException(); + }), + ); await expectLater( client.send(AbortableRequest('GET', uri, abortTrigger: Future.value())), @@ -86,36 +93,41 @@ void main() { final responseStream = StreamController(); var aborted = false; - final (client, _) = - await createRemoteClient(MockClient.streaming((request, _) async { - (request as Abortable).abortTrigger!.whenComplete(() { - aborted = true; - responseStream - ..addError(RequestAbortedException()) - ..close(); - }); - return StreamedResponse(responseStream.stream, 200); - })); + final (client, _) = await createRemoteClient( + MockClient.streaming((request, _) async { + (request as Abortable).abortTrigger!.whenComplete(() { + aborted = true; + responseStream + ..addError(RequestAbortedException()) + ..close(); + }); + return StreamedResponse(responseStream.stream, 200); + }), + ); - final response = await client - .send(AbortableRequest('GET', uri, abortTrigger: abort.future)); + final response = await client.send( + AbortableRequest('GET', uri, abortTrigger: abort.future), + ); responseStream.add(Uint8List(42)); final receivedResponseStream = StreamQueue(response.stream); await expectLater(receivedResponseStream, emits(hasLength(42))); abort.complete(); await expectLater( - receivedResponseStream, emitsError(isA())); + receivedResponseStream, + emitsError(isA()), + ); expect(aborted, isTrue); }); test('via stream cancel', () async { final responseStream = StreamController(); - final (client, _) = - await createRemoteClient(MockClient.streaming((request, _) async { - return StreamedResponse(responseStream.stream, 200); - })); + final (client, _) = await createRemoteClient( + MockClient.streaming((request, _) async { + return StreamedResponse(responseStream.stream, 200); + }), + ); final response = await client.send(AbortableRequest('GET', uri)); responseStream.add(Uint8List(42)); @@ -133,22 +145,26 @@ void main() { late Client client; late WorkerCommunicationChannel channel; - (client, channel) = - await createRemoteClient(MockClient.streaming((request, _) async { - channel.close(); - return StreamedResponse(Stream.empty(), 200); - })); + (client, channel) = await createRemoteClient( + MockClient.streaming((request, _) async { + channel.close(); + return StreamedResponse(Stream.empty(), 200); + }), + ); - await expectLater(client.send(AbortableRequest('GET', uri)), - throwsA(isA())); + await expectLater( + client.send(AbortableRequest('GET', uri)), + throwsA(isA()), + ); }); test('in response stream', () async { final responseStream = StreamController(); - final (client, channel) = - await createRemoteClient(MockClient.streaming((request, _) async { - return StreamedResponse(responseStream.stream, 200); - })); + final (client, channel) = await createRemoteClient( + MockClient.streaming((request, _) async { + return StreamedResponse(responseStream.stream, 200); + }), + ); final response = await client.send(AbortableRequest('GET', uri)); responseStream.add(Uint8List(42)); @@ -156,7 +172,9 @@ void main() { await expectLater(receivedResponseStream, emits(hasLength(42))); final expectation = expectLater( - receivedResponseStream, emitsError(isA())); + receivedResponseStream, + emitsError(isA()), + ); await pumpEventQueue(); channel.close(); await expectation; @@ -165,7 +183,8 @@ void main() { } Future<(Client, WorkerCommunicationChannel)> createRemoteClient( - Client original) async { + Client original, +) async { final channel = MessageChannel(); final local = WorkerCommunicationChannel( diff --git a/packages/powersync/test/web/sync_worker_test.dart b/packages/powersync/test/web/sync_worker_test.dart index 8939bd9e..4e6daad5 100644 --- a/packages/powersync/test/web/sync_worker_test.dart +++ b/packages/powersync/test/web/sync_worker_test.dart @@ -56,14 +56,22 @@ void main() { test('aborts sync when database is closed', () async { final handle = createWorkerHandle(connector: _ThrowingBackendConnector()); final hasError = expectLater( - db.statusStream, - emitsThrough(isA() - .having((e) => e.downloadError, 'downloadError', isNotNull))); + db.statusStream, + emitsThrough( + isA().having( + (e) => e.downloadError, + 'downloadError', + isNotNull, + ), + ), + ); await handle.streamingSync(); await hasError; - expect(db.currentStatus.downloadError.toString(), - contains('Expected error from fetchCredentials')); + expect( + db.currentStatus.downloadError.toString(), + contains('Expected error from fetchCredentials'), + ); final syncRunner = syncWorker.requestedSyncTasks.values.single; expect(syncRunner.sync, isNotNull); expect(syncRunner.connections, hasLength(1)); @@ -79,18 +87,20 @@ void main() { test('handles tabs closing while serving a request', () async { late SyncWorkerHandle handle; final didRequestCredentials = Completer(); - handle = createWorkerHandle(connector: _ThrowingBackendConnector(() { - // When the fetchCredentials request is sent, there should be a sync - // process. - final syncRunner = syncWorker.requestedSyncTasks.values.single; - expect(syncRunner.sync, isNotNull); - expect(syncRunner.connections, hasLength(1)); - - // Close the handle while the fetchCredentials request is active, meaning - // the sync worker will never receive a response. - handle.closeChannel(); - didRequestCredentials.complete(); - })); + handle = createWorkerHandle( + connector: _ThrowingBackendConnector(() { + // When the fetchCredentials request is sent, there should be a sync + // process. + final syncRunner = syncWorker.requestedSyncTasks.values.single; + expect(syncRunner.sync, isNotNull); + expect(syncRunner.connections, hasLength(1)); + + // Close the handle while the fetchCredentials request is active, meaning + // the sync worker will never receive a response. + handle.closeChannel(); + didRequestCredentials.complete(); + }), + ); await handle.streamingSync(); await didRequestCredentials.future; @@ -112,7 +122,9 @@ void main() { final handle = createWorkerHandle( connector: TestConnector( () async => PowerSyncCredentials( - endpoint: 'http://test.powersync.example.org', token: 'token'), + endpoint: 'http://test.powersync.example.org', + token: 'token', + ), ), options: SyncOptions(httpClient: () => client), ); diff --git a/packages/powersync/tool/update_core_extension_hashes.dart b/packages/powersync/tool/update_core_extension_hashes.dart index 30cbf6d3..6073a01e 100644 --- a/packages/powersync/tool/update_core_extension_hashes.dart +++ b/packages/powersync/tool/update_core_extension_hashes.dart @@ -35,7 +35,8 @@ Future main() async { final newContents = StringBuffer(); newContents ..write( - originalContents.substring(0, originalContents.indexOf(startMarker))) + originalContents.substring(0, originalContents.indexOf(startMarker)), + ) ..writeln(startMarker); for (final (fileName, digest) in entries) { newContents @@ -43,25 +44,33 @@ Future main() async { ..writeln(" '$digest',"); } - newContents - .write(originalContents.substring(originalContents.indexOf(endMarker))); + newContents.write( + originalContents.substring(originalContents.indexOf(endMarker)), + ); client.close(); await sourceFile.writeAsString(newContents.toString()); } Future>> _fetchReleaseAssets( - http.Client client, String tag) async { + http.Client client, + String tag, +) async { final uri = Uri.parse( - 'https://api.github.com/repos/powersync-ja/powersync-sqlite-core/releases/tags/$tag'); - final response = await client.get(uri, headers: { - 'Accept': 'application/vnd.github+json', - 'User-Agent': 'powersync-dart-tool', - }); + 'https://api.github.com/repos/powersync-ja/powersync-sqlite-core/releases/tags/$tag', + ); + final response = await client.get( + uri, + headers: { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'powersync-dart-tool', + }, + ); if (response.statusCode != 200) { throw Exception( - 'GitHub API error ${response.statusCode} fetching release $tag'); + 'GitHub API error ${response.statusCode} fetching release $tag', + ); } final release = json.decode(response.body) as Map; From ba7cf743a29879decc4fa265557f85209a5489aa Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 21 Aug 2026 12:29:44 +0200 Subject: [PATCH 2/2] Fix analysis warnings --- packages/powersync/test/sync/utils.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/powersync/test/sync/utils.dart b/packages/powersync/test/sync/utils.dart index 06e542e3..bd443ed2 100644 --- a/packages/powersync/test/sync/utils.dart +++ b/packages/powersync/test/sync/utils.dart @@ -130,7 +130,7 @@ Object checkpointComplete({int? priority, String lastOpId = '1'}) { return { priority == null ? 'checkpoint_complete' : 'partial_checkpoint_complete': { 'last_op_id': lastOpId, - if (priority != null) 'priority': priority, + 'priority': ?priority, }, }; } @@ -147,6 +147,6 @@ Object bucketDescription( 'checksum': checksum, 'priority': priority, 'count': count, - if (subscriptions != null) 'subscriptions': subscriptions, + 'subscriptions': ?subscriptions, }; }