Skip to content

WIP: 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

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side-note, it could be useful to package some of these widgets in a separate lib. Doesn't have to cover all use cases - I'm thinking just making a couple of sync-related widgets available, to make it easier to get started.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good idea 👍 Given that we already have package:powersync as a Flutter-only library, I think it could reasonably live there (or at least be exported from there, we might want a package:powersync_widgets to share code between the main package and the SQLCipher one). It just can't be in package:powersync_core so that we can keep using the package without Flutter.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important for right now, but is there a good way to combine the GuardBySync initial-sync progress, and the query progress indicator? It would be nice if there could be a single common widget handling both, instead of having separate progress indicators.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we could definitely have some kind of query widget that takes SQL + parameters and then shows:

  1. A linear progress indicator until the first sync.
  2. A circular progress indicator while the query is running.
  3. Then finally an inner builder rendering the actual data in all other cases.

This might need some more thought put into it to combine it with e.g. drift, but the basic pattern is always the same (guard for first sync, guard for data on stream, show results). I'm not sure if an abstraction is worth it here or if it'll end up complacting the use-sites further, but it's worth trying out.

}
},
),
);
}

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