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/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 eb0341a1..00cadc9e 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; @@ -297,8 +329,9 @@ class PowerSyncDatabase with SqliteQueries implements SqliteConnection { void _setStatus(SyncStatus status) { if (status != currentStatus) { - currentStatus = status; - _statusStreamController.add(status); + currentStatus = status.copyWith( + hasSynced: status.hasSynced ?? status.lastSyncedAt != null); + _statusStreamController.add(currentStatus); } } diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart index 880bee12..e7c10097 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 diff --git a/packages/powersync/lib/src/sync_status.dart b/packages/powersync/lib/src/sync_status.dart index 4ad7152c..a1f4a543 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,30 @@ class SyncStatus { other.connecting == connecting && other.downloadError == downloadError && other.uploadError == uploadError && - other.lastSyncedAt == lastSyncedAt); + other.lastSyncedAt == lastSyncedAt && + 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]. @@ -68,7 +96,7 @@ class SyncStatus { @override String toString() { - return "SyncStatus"; + return "SyncStatus"; } } 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.