From ec2086fcd080f0e5d02ff235217c7cf24dd6a7b0 Mon Sep 17 00:00:00 2001 From: Mugi Khan Date: Mon, 1 Jul 2024 18:44:44 +0200 Subject: [PATCH 1/3] Add hasSynced and waitForFirstSync --- .../powersync/lib/src/powersync_database.dart | 32 +++++++++++++++++++ .../powersync/lib/src/streaming_sync.dart | 5 +++ packages/powersync/lib/src/sync_status.dart | 10 ++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/powersync/lib/src/powersync_database.dart b/packages/powersync/lib/src/powersync_database.dart index eb0341a1..54b41baf 100644 --- a/packages/powersync/lib/src/powersync_database.dart +++ b/packages/powersync/lib/src/powersync_database.dart @@ -153,6 +153,7 @@ class PowerSyncDatabase with SqliteQueries implements SqliteConnection { await database.initialize(); await database.execute('SELECT powersync_init()'); await updateSchema(schema); + await _updateHasSynced(); } /// Replace the schema with a new version. @@ -175,6 +176,37 @@ class PowerSyncDatabase with SqliteQueries implements SqliteConnection { return _initialized; } + Future _updateHasSynced() async { + const syncedSQL = + 'SELECT 1 FROM ps_buckets WHERE last_applied_op > 0 LIMIT 1'; + + // Query the database to see if any data has been synced. + final result = await database.execute(syncedSQL); + final hasSynced = result.rows.isNotEmpty; + + if (hasSynced != currentStatus.hasSynced) { + final status = SyncStatus(hasSynced: hasSynced); + _setStatus(status); + } + } + + /// + /// returns a [Future] which will resolve once the first full sync has completed. + /// + Future waitForFirstSync() async { + if (currentStatus.hasSynced ?? false) { + return; + } + final completer = Completer(); + statusStream.listen((result) { + if (result.hasSynced ?? false) { + completer.complete(); + } + }); + + return completer.future; + } + @override bool get closed { return database.closed; diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart index 880bee12..f3b20481 100644 --- a/packages/powersync/lib/src/streaming_sync.dart +++ b/packages/powersync/lib/src/streaming_sync.dart @@ -153,6 +153,7 @@ class StreamingSyncImplementation { /// To clear errors, use [_noError] instead of null. void _updateStatus( {DateTime? lastSyncedAt, + bool? hasSynced, bool? connected, bool? connecting, bool? downloading, @@ -164,6 +165,7 @@ class StreamingSyncImplementation { connected: c, connecting: !c && (connecting ?? lastStatus.connecting), lastSyncedAt: lastSyncedAt ?? lastStatus.lastSyncedAt, + hasSynced: hasSynced ?? lastStatus.hasSynced, downloading: downloading ?? lastStatus.downloading, uploading: uploading ?? lastStatus.uploading, uploadError: uploadError == _noError @@ -232,6 +234,7 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, + hasSynced: true, lastSyncedAt: DateTime.now()); } @@ -293,6 +296,7 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, + hasSynced: true, lastSyncedAt: DateTime.now()); } else if (validatedCheckpoint == targetCheckpoint) { final result = await adapter.syncLocalDatabase(targetCheckpoint!); @@ -310,6 +314,7 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, + hasSynced: true, lastSyncedAt: DateTime.now()); } } diff --git a/packages/powersync/lib/src/sync_status.dart b/packages/powersync/lib/src/sync_status.dart index 4ad7152c..6b5a4332 100644 --- a/packages/powersync/lib/src/sync_status.dart +++ b/packages/powersync/lib/src/sync_status.dart @@ -24,6 +24,10 @@ class SyncStatus { /// Currently this is reset to null after a restart. final DateTime? lastSyncedAt; + /// Indicates whether there has been at least one full sync, if any. + /// Is null when unknown, for example when state is still being loaded from the database. + final bool? hasSynced; + /// Error during uploading. /// /// Cleared on the next successful upload. @@ -38,6 +42,7 @@ class SyncStatus { {this.connected = false, this.connecting = false, this.lastSyncedAt, + this.hasSynced, this.downloading = false, this.uploading = false, this.downloadError, @@ -52,7 +57,8 @@ class SyncStatus { other.connecting == connecting && other.downloadError == downloadError && other.uploadError == uploadError && - other.lastSyncedAt == lastSyncedAt); + other.lastSyncedAt == lastSyncedAt && + other.hasSynced == hasSynced); } /// Get the current [downloadError] or [uploadError]. @@ -68,7 +74,7 @@ class SyncStatus { @override String toString() { - return "SyncStatus"; + return "SyncStatus"; } } From d4b0b526d8e4452fef09bb1e3da3b6dc8a485ce3 Mon Sep 17 00:00:00 2001 From: Mugi Khan Date: Tue, 2 Jul 2024 10:47:14 +0200 Subject: [PATCH 2/3] Export sqlite3_common --- packages/powersync/lib/sqlite3_common.dart | 5 +++++ .../powersync/lib/src/powersync_database.dart | 3 ++- .../powersync/lib/src/streaming_sync.dart | 3 --- packages/powersync/lib/src/sync_status.dart | 22 +++++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/powersync/lib/sqlite3_common.dart diff --git a/packages/powersync/lib/sqlite3_common.dart b/packages/powersync/lib/sqlite3_common.dart new file mode 100644 index 00000000..df84a8e0 --- /dev/null +++ b/packages/powersync/lib/sqlite3_common.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_common](https://pub.dev/packages/sqlite3) to expose sqlite3_common without +/// adding it as a direct dependency. +library; + +export 'package:sqlite_async/sqlite3_common.dart'; diff --git a/packages/powersync/lib/src/powersync_database.dart b/packages/powersync/lib/src/powersync_database.dart index 54b41baf..15ff6760 100644 --- a/packages/powersync/lib/src/powersync_database.dart +++ b/packages/powersync/lib/src/powersync_database.dart @@ -329,7 +329,8 @@ class PowerSyncDatabase with SqliteQueries implements SqliteConnection { void _setStatus(SyncStatus status) { if (status != currentStatus) { - currentStatus = status; + currentStatus = status.copyWith( + hasSynced: status.hasSynced ?? status.lastSyncedAt != null); _statusStreamController.add(status); } } diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart index f3b20481..e7c10097 100644 --- a/packages/powersync/lib/src/streaming_sync.dart +++ b/packages/powersync/lib/src/streaming_sync.dart @@ -234,7 +234,6 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, - hasSynced: true, lastSyncedAt: DateTime.now()); } @@ -296,7 +295,6 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, - hasSynced: true, lastSyncedAt: DateTime.now()); } else if (validatedCheckpoint == targetCheckpoint) { final result = await adapter.syncLocalDatabase(targetCheckpoint!); @@ -314,7 +312,6 @@ class StreamingSyncImplementation { _updateStatus( downloading: false, downloadError: _noError, - hasSynced: true, lastSyncedAt: DateTime.now()); } } diff --git a/packages/powersync/lib/src/sync_status.dart b/packages/powersync/lib/src/sync_status.dart index 6b5a4332..a1f4a543 100644 --- a/packages/powersync/lib/src/sync_status.dart +++ b/packages/powersync/lib/src/sync_status.dart @@ -61,6 +61,28 @@ class SyncStatus { other.hasSynced == hasSynced); } + SyncStatus copyWith({ + bool? connected, + bool? downloading, + bool? uploading, + bool? connecting, + Object? uploadError, + Object? downloadError, + DateTime? lastSyncedAt, + bool? hasSynced, + }) { + return SyncStatus( + connected: connected ?? this.connected, + downloading: downloading ?? this.downloading, + uploading: uploading ?? this.uploading, + connecting: connecting ?? this.connecting, + uploadError: uploadError ?? this.uploadError, + downloadError: downloadError ?? this.downloadError, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + hasSynced: hasSynced ?? this.hasSynced, + ); + } + /// Get the current [downloadError] or [uploadError]. Object? get anyError { return downloadError ?? uploadError; From 51bc04bd413e2dd4aab70b50b6937159383da214 Mon Sep 17 00:00:00 2001 From: Mugi Khan Date: Tue, 2 Jul 2024 12:01:52 +0200 Subject: [PATCH 3/3] Add example code for hasSynced Bump version --- .../lib/models/todo_list.dart | 5 ++++ .../lib/widgets/lists_page.dart | 25 ++++++++++++++----- packages/powersync/CHANGELOG.md | 5 ++++ .../powersync/lib/src/powersync_database.dart | 2 +- packages/powersync/pubspec.yaml | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/demos/supabase-todolist/lib/models/todo_list.dart b/demos/supabase-todolist/lib/models/todo_list.dart index 489d1631..af33908f 100644 --- a/demos/supabase-todolist/lib/models/todo_list.dart +++ b/demos/supabase-todolist/lib/models/todo_list.dart @@ -1,3 +1,4 @@ +import 'package:powersync/powersync.dart'; import 'package:powersync/sqlite3.dart' as sqlite; import './todo_item.dart'; @@ -59,6 +60,10 @@ class TodoList { }); } + static Stream watchSyncStatus() { + return db.statusStream; + } + /// Create a new list static Future create(String name) async { final results = await db.execute(''' diff --git a/demos/supabase-todolist/lib/widgets/lists_page.dart b/demos/supabase-todolist/lib/widgets/lists_page.dart index e31c2fc8..142d9e9f 100644 --- a/demos/supabase-todolist/lib/widgets/lists_page.dart +++ b/demos/supabase-todolist/lib/widgets/lists_page.dart @@ -52,7 +52,9 @@ class ListsWidget extends StatefulWidget { class _ListsWidgetState extends State { List _data = []; + bool hasSynced = false; StreamSubscription? _subscription; + StreamSubscription? _syncStatusSubscription; _ListsWidgetState(); @@ -68,21 +70,32 @@ class _ListsWidgetState extends State { _data = data; }); }); + _syncStatusSubscription = TodoList.watchSyncStatus().listen((status) { + if (!context.mounted) { + return; + } + setState(() { + hasSynced = status.hasSynced ?? false; + }); + }); } @override void dispose() { super.dispose(); _subscription?.cancel(); + _syncStatusSubscription?.cancel(); } @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), - ); + return !hasSynced + ? const Text("Busy with sync...") + : ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: _data.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); } } diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index a0d92ba2..d35754be 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.5.1 + +- Adds a hasSynced flag to check if initial data has been synced. +- Adds a waitForFirstSync method to check if the first full sync has completed. + ## 1.5.0 - Upgrade minimum Dart SDK constraint to `3.4.0`. diff --git a/packages/powersync/lib/src/powersync_database.dart b/packages/powersync/lib/src/powersync_database.dart index 15ff6760..00cadc9e 100644 --- a/packages/powersync/lib/src/powersync_database.dart +++ b/packages/powersync/lib/src/powersync_database.dart @@ -331,7 +331,7 @@ class PowerSyncDatabase with SqliteQueries implements SqliteConnection { if (status != currentStatus) { currentStatus = status.copyWith( hasSynced: status.hasSynced ?? status.lastSyncedAt != null); - _statusStreamController.add(status); + _statusStreamController.add(currentStatus); } } diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index 38d2cecd..48a72f4d 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync -version: 1.5.0 +version: 1.5.1 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Flutter SDK - keep PostgreSQL databases in sync with on-device SQLite databases.