Skip to content

Sync progress status #260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions demos/django-todolist/lib/widgets/guard_by_sync.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart' hide Column;
import 'package:powersync_django_todolist_demo/powersync.dart';

/// A widget that shows [child] after a complete sync on the database has
/// completed and a progress bar before that.
class GuardBySync extends StatelessWidget {
final Widget child;

/// When set, wait only for a complete sync within the [BucketPriority]
/// instead of a full sync.
final BucketPriority? priority;

const GuardBySync({
super.key,
required this.child,
this.priority,
});

@override
Widget build(BuildContext context) {
return StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) {
final status = snapshot.requireData;
final (didSync, progress) = switch (priority) {
null => (
status.hasSynced ?? false,
status.downloadProgress?.untilCompletion
),
var priority? => (
status.statusForPriority(priority).hasSynced ?? false,
status.downloadProgress?.untilPriority(priority)
),
};

if (didSync) {
return child;
} else {
return Center(
child: Column(
children: [
const Text('Busy with sync...'),
LinearProgressIndicator(value: progress?.fraction),
if (progress case final progress?)
Text('${progress.completed} out of ${progress.total}')
],
),
);
}
},
);
}
}
58 changes: 18 additions & 40 deletions demos/django-todolist/lib/widgets/lists_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:powersync_django_todolist_demo/widgets/guard_by_sync.dart';

import './list_item.dart';
import './list_item_dialog.dart';
Expand Down Expand Up @@ -41,48 +40,27 @@ class ListsPage extends StatelessWidget {
}
}

class ListsWidget extends StatefulWidget {
final class ListsWidget extends StatelessWidget {
const ListsWidget({super.key});

@override
State<StatefulWidget> createState() {
return _ListsWidgetState();
}
}

class _ListsWidgetState extends State<ListsWidget> {
List<TodoList> _data = [];
StreamSubscription? _subscription;

_ListsWidgetState();

@override
void initState() {
super.initState();
final stream = TodoList.watchListsWithStats();
_subscription = stream.listen((data) {
if (!context.mounted) {
return;
}
setState(() {
_data = data;
});
});
}

@override
void dispose() {
super.dispose();
_subscription?.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 GuardBySync(
child: StreamBuilder(
stream: TodoList.watchListsWithStats(),
builder: (context, snapshot) {
if (snapshot.data case final todoLists?) {
return ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: todoLists.map((list) {
return ListItemWidget(list: list);
}).toList(),
);
} else {
return const CircularProgressIndicator();
}
},
),
);
}
}
4 changes: 2 additions & 2 deletions demos/django-todolist/macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.1)
- sqlite3 (~> 3.49.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
Expand Down Expand Up @@ -61,7 +61,7 @@ SPEC CHECKSUMS:
powersync_flutter_libs: 011c1704766d154faf2373bb9c973d26910d322b
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832

PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
Expand Down
53 changes: 53 additions & 0 deletions demos/supabase-todolist/lib/widgets/guard_by_sync.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart' hide Column;
import 'package:powersync_flutter_demo/powersync.dart';

/// A widget that shows [child] after a complete sync on the database has
/// completed and a progress bar before that.
class GuardBySync extends StatelessWidget {
final Widget child;

/// When set, wait only for a complete sync within the [BucketPriority]
/// instead of a full sync.
final BucketPriority? priority;

const GuardBySync({
super.key,
required this.child,
this.priority,
});

@override
Widget build(BuildContext context) {
return StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) {
final status = snapshot.requireData;
final (didSync, progress) = switch (priority) {
null => (status.hasSynced ?? false, status.downloadProgress),
var priority? => (
status.statusForPriority(priority).hasSynced ?? false,
status.downloadProgress?.untilPriority(priority)
),
};

if (didSync) {
return child;
} else {
return Center(
child: Column(
children: [
const Text('Busy with sync...'),
LinearProgressIndicator(value: progress?.downloadedFraction),
if (progress case final progress?)
Text(
'${progress.downloadedOperations} out of ${progress.totalOperations}')
],
),
);
}
},
);
}
}
42 changes: 18 additions & 24 deletions demos/supabase-todolist/lib/widgets/lists_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart';
import 'package:powersync_flutter_demo/powersync.dart';
import 'package:powersync_flutter_demo/widgets/guard_by_sync.dart';

import './list_item.dart';
import './list_item_dialog.dart';
Expand Down Expand Up @@ -46,29 +46,23 @@ final class ListsWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: db.waitForFirstSync(priority: _listsPriority),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return StreamBuilder(
stream: TodoList.watchListsWithStats(),
builder: (context, snapshot) {
if (snapshot.data case final todoLists?) {
return ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: todoLists.map((list) {
return ListItemWidget(list: list);
}).toList(),
);
} else {
return const CircularProgressIndicator();
}
},
);
} else {
return const Text('Busy with sync...');
}
},
return GuardBySync(
priority: _listsPriority,
child: StreamBuilder(
stream: TodoList.watchListsWithStats(),
builder: (context, snapshot) {
if (snapshot.data case final todoLists?) {
return ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: todoLists.map((list) {
return ListItemWidget(list: list);
}).toList(),
);
} else {
return const CircularProgressIndicator();
}
},
),
);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/powersync_core/lib/powersync_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export 'src/exceptions.dart';
export 'src/log.dart';
export 'src/open_factory.dart';
export 'src/schema.dart';
export 'src/sync_status.dart';
export 'src/sync/sync_status.dart'
hide BucketProgress, InternalSyncDownloadProgress;
export 'src/uuid.dart';
2 changes: 1 addition & 1 deletion packages/powersync_core/lib/src/database/core_version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ extension type const PowerSyncCoreVersion((int, int, int) _tuple) {
// Note: When updating this, also update the download URL in
// scripts/init_powersync_core_binary.dart and the version ref in
// packages/sqlite3_wasm_build/build.sh
static const minimum = PowerSyncCoreVersion((0, 3, 11));
static const minimum = PowerSyncCoreVersion((0, 3, 12));

/// The first version of the core extensions that this version of the Dart
/// SDK doesn't support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:meta/meta.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:powersync_core/src/abort_controller.dart';
import 'package:powersync_core/src/bucket_storage.dart';
import 'package:powersync_core/src/sync/bucket_storage.dart';
import 'package:powersync_core/src/connector.dart';
import 'package:powersync_core/src/database/powersync_database.dart';
import 'package:powersync_core/src/database/powersync_db_mixin.dart';
Expand All @@ -16,8 +16,8 @@ import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.
import 'package:powersync_core/src/open_factory/native/native_open_factory.dart';
import 'package:powersync_core/src/schema.dart';
import 'package:powersync_core/src/schema_logic.dart';
import 'package:powersync_core/src/streaming_sync.dart';
import 'package:powersync_core/src/sync_status.dart';
import 'package:powersync_core/src/sync/streaming_sync.dart';
import 'package:powersync_core/src/sync/sync_status.dart';
import 'package:sqlite_async/sqlite3_common.dart';
import 'package:sqlite_async/sqlite_async.dart';

Expand Down
33 changes: 22 additions & 11 deletions packages/powersync_core/lib/src/database/powersync_db_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:powersync_core/src/database/core_version.dart';
import 'package:powersync_core/src/powersync_update_notification.dart';
import 'package:powersync_core/src/schema.dart';
import 'package:powersync_core/src/schema_logic.dart';
import 'package:powersync_core/src/sync_status.dart';
import 'package:powersync_core/src/sync/sync_status.dart';

mixin PowerSyncDatabaseMixin implements SqliteConnection {
/// Schema used for the local database.
Expand Down Expand Up @@ -175,16 +175,27 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
@visibleForTesting
void setStatus(SyncStatus status) {
if (status != currentStatus) {
// Note that currently the streaming sync implementation will never set hasSynced.
// lastSyncedAt implies that syncing has completed at some point (hasSynced = true).
// The previous values of hasSynced should be preserved here.
final newStatus = status.copyWith(
hasSynced: status.lastSyncedAt != null
? true
: status.hasSynced ?? currentStatus.hasSynced,
lastSyncedAt: status.lastSyncedAt ?? currentStatus.lastSyncedAt);
// If the absence of hasSync was the only difference, the new states would be equal
// and don't require an event. So, check again.
final newStatus = SyncStatus(
connected: status.connected,
downloading: status.downloading,
uploading: status.uploading,
connecting: status.connecting,
uploadError: status.uploadError,
downloadError: status.downloadError,
priorityStatusEntries: status.priorityStatusEntries,
downloadProgress: status.downloadProgress,
// Note that currently the streaming sync implementation will never set
// hasSynced. lastSyncedAt implies that syncing has completed at some
// point (hasSynced = true).
// The previous values of hasSynced should be preserved here.
lastSyncedAt: status.lastSyncedAt ?? currentStatus.lastSyncedAt,
hasSynced: status.lastSyncedAt != null
? true
: status.hasSynced ?? currentStatus.hasSynced,
);

// If the absence of hasSynced was the only difference, the new states
// would be equal and don't require an event. So, check again.
if (newStatus != currentStatus) {
currentStatus = newStatus;
statusStreamController.add(currentStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import 'package:meta/meta.dart';
import 'package:fetch_client/fetch_client.dart';
import 'package:logging/logging.dart';
import 'package:powersync_core/src/abort_controller.dart';
import 'package:powersync_core/src/bucket_storage.dart';
import 'package:powersync_core/src/sync/bucket_storage.dart';
import 'package:powersync_core/src/connector.dart';
import 'package:powersync_core/src/database/powersync_database.dart';
import 'package:powersync_core/src/database/powersync_db_mixin.dart';
import 'package:powersync_core/src/log.dart';
import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart';
import 'package:powersync_core/src/open_factory/web/web_open_factory.dart';
import 'package:powersync_core/src/schema.dart';
import 'package:powersync_core/src/streaming_sync.dart';
import 'package:powersync_core/src/sync/streaming_sync.dart';
import 'package:sqlite_async/sqlite_async.dart';
import 'package:powersync_core/src/schema_logic.dart' as schema_logic;

Expand Down
Loading
Loading