Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions demos/supabase-todolist-drift/lib/app_config_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ class AppConfig {
static const String powersyncUrl = 'https://foo.powersync.journeyapps.com';
static const String supabaseStorageBucket =
''; // Optional. Only required when syncing attachments and using Supabase Storage. See packages/powersync_attachments_helper.

// Whether to enable swipe-to-refresh through through checkpoint requests
// (https://docs.powersync.com/client-sdks/advanced/checkpoint-requests).
static const bool enableCheckpointRequests = false;
}
48 changes: 47 additions & 1 deletion demos/supabase-todolist-drift/lib/components/app_bar.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:powersync/powersync.dart';

import '../app_config.dart';
import '../powersync/powersync.dart';
import '../screens/search.dart';
import '../stores/refresh.dart';

final appBar = AppBar(
title: const Text('PowerSync Flutter Demo'),
Expand All @@ -28,6 +31,7 @@ final class StatusAppBar extends ConsumerWidget implements PreferredSizeWidget {
leading: const AutoLeadingButton(),
title: title,
actions: <Widget>[
const _RefreshButton(),
IconButton(
onPressed: () {
showSearch(context: context, delegate: FtsSearchDelegate());
Expand Down Expand Up @@ -142,3 +146,45 @@ class _AutoLeadingButtonState extends State<AutoLeadingButton> {
setState(() {});
}
}

/// A refresh button implemented with [Sync Catch-Up](https://docs.powersync.com/client-sdks/advanced/checkpoint-requests).
class _RefreshButton extends HookConsumerWidget {
const _RefreshButton();

@override
Widget build(BuildContext context, WidgetRef ref) {
if (!AppConfig.enableCheckpointRequests) {
return const SizedBox.shrink();
}

final canRefreshNow = ref.watch(canRefresh);
final isRefreshing = ref.watch(refresh).isPending;
final controller = useAnimationController(
duration: const Duration(seconds: 1),
);

useEffect(() {
if (isRefreshing) {
controller.repeat();
} else {
controller.reset();
}
return null;
}, [isRefreshing]);

void triggerRefresh() {
syncNow(ref);
}

return Tooltip(
message: 'Sync now',
child: IconButton(
onPressed: canRefreshNow ? triggerRefresh : null,
icon: RotationTransition(
turns: controller,
child: const Icon(Icons.refresh),
),
),
);
}
}
70 changes: 68 additions & 2 deletions demos/supabase-todolist-drift/lib/components/page_layout.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import 'dart:async';

import 'package:auto_route/auto_route.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:material_ui/material_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod/experimental/mutation.dart';

import '../app_config.dart';
import '../navigation.gr.dart';
import '../stores/refresh.dart';
import '../supabase.dart';
import 'app_bar.dart';

final class PageLayout extends ConsumerWidget {
final class PageLayout extends HookConsumerWidget {
final Widget content;
final Widget? title;
final Widget? floatingActionButton;
Expand All @@ -22,11 +29,70 @@ final class PageLayout extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
Widget body = Center(child: content);

if (AppConfig.enableCheckpointRequests) {
final refreshIndicatorKey =
useMemoized(() => GlobalKey<RefreshIndicatorState>());
final isRefreshing = ref.watch(refresh).isPending;
final refreshingBefore = usePrevious(isRefreshing) ?? false;
final scaffoldMessenger = ScaffoldMessenger.of(context);

useEffect(() {
// Show the refresh indicator when a refresh is triggered.
if (!refreshingBefore && isRefreshing) {
refreshIndicatorKey.currentState?.show();
}

// Show a status snackbar on completed sync.
if (refreshingBefore && !isRefreshing) {
SchedulerBinding.instance.addPostFrameCallback((_) {
switch (ref.read(refresh)) {
case MutationError():
scaffoldMessenger
.showSnackBar(SnackBar(content: Text('Sync failed.')));
case MutationSuccess():
scaffoldMessenger.showSnackBar(
SnackBar(content: Text('Sync refresh complete!')));
default:
break;
}
});
}
return null;
}, [refreshIndicatorKey, refreshingBefore, isRefreshing]);

body = RefreshIndicator(
key: refreshIndicatorKey,
onRefresh: () {
// This is called both when a child scrollable is swiped to refresh,
// and by the useEffect hook above. Depending on what triggered it, we
// either request a checkpoint or wait for the previous one to
// complete.
final isPending = ref.read(refresh).isPending;
if (isPending) {
final completer = Completer<void>();
late final ProviderSubscription<MutationState> subscription;
subscription = ref.listenManual(refresh, (previous, next) {
if (!next.isPending) {
subscription.close();
completer.complete();
}
});
return completer.future;
} else {
return syncNow(ref);
}
},
child: body,
);
}

return Scaffold(
appBar: StatusAppBar(
title: title ?? const Text('PowerSync Demo'),
),
body: Center(child: content),
body: body,
floatingActionButton: floatingActionButton,
drawer: showDrawer
? Drawer(
Expand Down
2 changes: 1 addition & 1 deletion demos/supabase-todolist-drift/lib/navigation.gr.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions demos/supabase-todolist-drift/lib/powersync/powersync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:stream_transform/stream_transform.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import '../app_config.dart';
import '../supabase.dart';
import 'connector.dart';
import 'schema.dart';
Expand All @@ -22,18 +23,25 @@ Future<PowerSyncDatabase> powerSyncInstance(Ref ref) async {
);
await db.initialize();

final syncOptions = SyncOptions(
checkpointMode: AppConfig.enableCheckpointRequests
// ignore: experimental_member_use
? CheckpointMode.requests()
: const CheckpointMode.legacy(),
);

SupabaseConnector? currentConnector;
if (ref.read(sessionProvider).value != null) {
currentConnector = SupabaseConnector();
db.connect(connector: currentConnector);
db.connect(connector: currentConnector, options: syncOptions);
}

final instance = Supabase.instance.client.auth;
final sub = instance.onAuthStateChange.listen((data) async {
final event = data.event;
if (event == AuthChangeEvent.signedIn) {
currentConnector = SupabaseConnector();
db.connect(connector: currentConnector!);
db.connect(connector: currentConnector!, options: syncOptions);
} else if (event == AuthChangeEvent.signedOut) {
currentConnector = null;
await db.disconnect();
Expand Down
30 changes: 30 additions & 0 deletions demos/supabase-todolist-drift/lib/stores/refresh.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ignore_for_file: experimental_member_use

import 'package:riverpod/experimental/mutation.dart';
import 'package:riverpod/riverpod.dart';

import '../app_config.dart';
import '../powersync/powersync.dart';

final refresh = Mutation(label: 'explicit sync');

/// Whether a checkpoint request can be sent from the current database state.
///
/// This is the case if checkpoint requests are enabled and the database is
/// connected. For this demo, we also avoid concurrent checkpoint refresh runs.
final canRefresh = Provider<bool>((ref) {
if (!AppConfig.enableCheckpointRequests) return false;

if (ref.watch(refresh).isPending) return false;

final status = ref.watch(syncStatus);
return status.connected || status.connecting;
});

Future<void> syncNow(MutationTarget target) {
return refresh.run(target, (tx) async {
final powersync = await tx.get(powerSyncInstanceProvider.future);
final checkpoint = await powersync.requestCheckpoint();
await checkpoint.waitForSync();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -545,7 +545,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -595,7 +595,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down

This file was deleted.

This file was deleted.

2 changes: 2 additions & 0 deletions packages/powersync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Add `SyncOptions.checkpointMode`, which can be used to replace a legacy implementation for write checkpoints
with a newer and more efficient endpoint.
- Add `PowerSyncDatabase.requestCheckpoint`, which can be used to "sync now": The checkpoint completes once all
source data created before the checkpoint has synced.
- Reduce log noise from attachments service ([#408](https://github.com/powersync-ja/powersync.dart/issues/408)).
- Fix errors that can't be sent across isolates disrupting the sync isolate.
- Update PowerSync SQLite core extension to 0.5.3, improving error messages for failed internal SQL statements.
Expand Down
1 change: 1 addition & 0 deletions packages/powersync/lib/powersync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export 'src/database/encryption_options.dart';
export 'src/exceptions.dart';
export 'src/log.dart';
export 'src/schema.dart';
export 'src/sync/checkpoint_request.dart' show CheckpointRequestException;
export 'src/sync/options.dart'
hide ResolvedSyncOptions, LegacyCheckpointMode, RequestsCheckpointMode;
export 'src/sync/stream.dart' hide CoreActiveStreamSubscription;
Expand Down
Loading