diff --git a/CHANGELOG.md b/CHANGELOG.md index e646bb26..a4307c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2024-03-05 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync` - `v1.3.0-alpha.3`](#powersync---v130-alpha3) + - [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.3.0-alpha.2` + +--- + +#### `powersync` - `v1.3.0-alpha.3` + + - Fixed issue where disconnectAndClear would prevent subsequent sync connection on native platforms and would fail to clear the database on web. + + ## 2024-02-15 ### Changes diff --git a/demos/supabase-anonymous-auth/pubspec.lock b/demos/supabase-anonymous-auth/pubspec.lock index 10331e82..6bb0d8da 100644 --- a/demos/supabase-anonymous-auth/pubspec.lock +++ b/demos/supabase-anonymous-auth/pubspec.lock @@ -156,10 +156,10 @@ packages: dependency: transitive description: name: gotrue - sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9 + sha256: "1bf6354278a98b8a1867263e94921da8a239de07e9babceab2b4e80af651a098" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" gtk: dependency: transitive description: @@ -332,25 +332,25 @@ packages: dependency: transitive description: name: postgrest - sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181" + sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.1" + version: "1.3.0-alpha.2" realtime_client: dependency: transitive description: name: realtime_client - sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03" + sha256: "41d6c5e0327d6c270b98b79bfed672928244af60e2856770f3eff697f9efe459" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" retry: dependency: transitive description: @@ -411,10 +411,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: "direct main" description: name: sqlite_async - sha256: "91f454cddc85617bea2c7c1544ff386887d0d2cf0ecdb3599015c05cc141ff4d" + sha256: "7a68036b2fc2fae5fc0efbc6bd436deb79e39090282348d979e6a7c38c8d067e" url: "https://pub.dev" source: hosted - version: "0.7.0-alpha.1" + version: "0.7.0-alpha.2" stack_trace: dependency: transitive description: @@ -504,18 +504,18 @@ packages: dependency: transitive description: name: supabase - sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40" + sha256: f431753d2a4cb9dacd72c7378154f806c2b2cef23859bd9cee1add23821e874d url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "5ef71289c380b6429216e941c69971c75eaab50d67fd7b540f6c1f6ebfc00ed7" + sha256: "30e966b89ee61dc9de845e2d7e1c60967b3189c410d105c6d42f09b6259f4cb6" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" term_glyph: dependency: transitive description: @@ -592,18 +592,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.0" url_launcher_windows: dependency: transitive description: @@ -670,4 +670,4 @@ packages: version: "2.0.0" sdks: dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.13.0" diff --git a/demos/supabase-anonymous-auth/pubspec.yaml b/demos/supabase-anonymous-auth/pubspec.yaml index f1b08e5e..571d190b 100644 --- a/demos/supabase-anonymous-auth/pubspec.yaml +++ b/demos/supabase-anonymous-auth/pubspec.yaml @@ -11,12 +11,12 @@ dependencies: flutter: sdk: flutter - powersync: ^1.3.0-alpha.2 + powersync: ^1.3.0-alpha.3 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.7.0-alpha.1 + sqlite_async: ^0.7.0-alpha.2 universal_io: ^2.2.2 dev_dependencies: diff --git a/demos/supabase-edge-function-auth/lib/widgets/login_page.dart b/demos/supabase-edge-function-auth/lib/widgets/login_page.dart index f4926361..ea493fac 100644 --- a/demos/supabase-edge-function-auth/lib/widgets/login_page.dart +++ b/demos/supabase-edge-function-auth/lib/widgets/login_page.dart @@ -34,7 +34,7 @@ class _LoginPageState extends State { await Supabase.instance.client.auth.signInWithPassword( email: _usernameController.text, password: _passwordController.text); - if (mounted) { + if (context.mounted) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => homePage, )); diff --git a/demos/supabase-edge-function-auth/lib/widgets/signup_page.dart b/demos/supabase-edge-function-auth/lib/widgets/signup_page.dart index d2e4219e..2f9150b4 100644 --- a/demos/supabase-edge-function-auth/lib/widgets/signup_page.dart +++ b/demos/supabase-edge-function-auth/lib/widgets/signup_page.dart @@ -34,7 +34,7 @@ class _SignupPageState extends State { final response = await Supabase.instance.client.auth.signUp( email: _usernameController.text, password: _passwordController.text); - if (mounted) { + if (context.mounted) { if (response.session != null) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => homePage, diff --git a/demos/supabase-edge-function-auth/pubspec.lock b/demos/supabase-edge-function-auth/pubspec.lock index 10331e82..6bb0d8da 100644 --- a/demos/supabase-edge-function-auth/pubspec.lock +++ b/demos/supabase-edge-function-auth/pubspec.lock @@ -156,10 +156,10 @@ packages: dependency: transitive description: name: gotrue - sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9 + sha256: "1bf6354278a98b8a1867263e94921da8a239de07e9babceab2b4e80af651a098" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" gtk: dependency: transitive description: @@ -332,25 +332,25 @@ packages: dependency: transitive description: name: postgrest - sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181" + sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.1" + version: "1.3.0-alpha.2" realtime_client: dependency: transitive description: name: realtime_client - sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03" + sha256: "41d6c5e0327d6c270b98b79bfed672928244af60e2856770f3eff697f9efe459" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" retry: dependency: transitive description: @@ -411,10 +411,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: "direct main" description: name: sqlite_async - sha256: "91f454cddc85617bea2c7c1544ff386887d0d2cf0ecdb3599015c05cc141ff4d" + sha256: "7a68036b2fc2fae5fc0efbc6bd436deb79e39090282348d979e6a7c38c8d067e" url: "https://pub.dev" source: hosted - version: "0.7.0-alpha.1" + version: "0.7.0-alpha.2" stack_trace: dependency: transitive description: @@ -504,18 +504,18 @@ packages: dependency: transitive description: name: supabase - sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40" + sha256: f431753d2a4cb9dacd72c7378154f806c2b2cef23859bd9cee1add23821e874d url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "5ef71289c380b6429216e941c69971c75eaab50d67fd7b540f6c1f6ebfc00ed7" + sha256: "30e966b89ee61dc9de845e2d7e1c60967b3189c410d105c6d42f09b6259f4cb6" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" term_glyph: dependency: transitive description: @@ -592,18 +592,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.0" url_launcher_windows: dependency: transitive description: @@ -670,4 +670,4 @@ packages: version: "2.0.0" sdks: dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.13.0" diff --git a/demos/supabase-edge-function-auth/pubspec.yaml b/demos/supabase-edge-function-auth/pubspec.yaml index 054d490c..f430d4e3 100644 --- a/demos/supabase-edge-function-auth/pubspec.yaml +++ b/demos/supabase-edge-function-auth/pubspec.yaml @@ -11,12 +11,12 @@ dependencies: flutter: sdk: flutter - powersync: ^1.3.0-alpha.2 + powersync: ^1.3.0-alpha.3 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.7.0-alpha.1 + sqlite_async: ^0.7.0-alpha.2 universal_io: ^2.2.2 dev_dependencies: diff --git a/demos/supabase-simple-chat/lib/pages/login_page.dart b/demos/supabase-simple-chat/lib/pages/login_page.dart index b973f09f..2d62f6ef 100644 --- a/demos/supabase-simple-chat/lib/pages/login_page.dart +++ b/demos/supabase-simple-chat/lib/pages/login_page.dart @@ -37,7 +37,7 @@ class LoginPageState extends State { } catch (_) { context.showErrorSnackBar(message: unexpectedErrorMessage); } - if (mounted) { + if (context.mounted) { setState(() { _isLoading = true; }); diff --git a/demos/supabase-simple-chat/pubspec.lock b/demos/supabase-simple-chat/pubspec.lock index 62fe7ea5..c678a36c 100644 --- a/demos/supabase-simple-chat/pubspec.lock +++ b/demos/supabase-simple-chat/pubspec.lock @@ -374,7 +374,7 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.1" + version: "1.3.0-alpha.2" realtime_client: dependency: transitive description: @@ -443,10 +443,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: @@ -656,18 +656,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.0" url_launcher_windows: dependency: transitive description: diff --git a/demos/supabase-simple-chat/pubspec.yaml b/demos/supabase-simple-chat/pubspec.yaml index e7a3630d..5f1012c9 100644 --- a/demos/supabase-simple-chat/pubspec.yaml +++ b/demos/supabase-simple-chat/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: supabase_flutter: ^1.10.25 timeago: ^3.6.0 - powersync: ^1.3.0-alpha.2 + powersync: ^1.3.0-alpha.3 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj index d274f929..16636b7a 100644 --- a/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..5e31d3d3 100644 --- a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { @@ -38,7 +41,17 @@ class _PhotoWidgetState extends State { bool fileExists = await File(photoPath).exists(); - return _ResolvedPhotoState(photoPath: photoPath, fileExists: fileExists); + final row = await attachmentQueue.db + .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); + + if (row != null) { + Attachment attachment = Attachment.fromRow(row); + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: attachment); + } + + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: null); } @override @@ -54,7 +67,7 @@ class _PhotoWidgetState extends State { Widget takePhotoButton = ElevatedButton( onPressed: () async { final camera = await setupCamera(); - if (!mounted) return; + if (!context.mounted) return; if (camera == null) { const snackBar = SnackBar( @@ -84,6 +97,20 @@ class _PhotoWidgetState extends State { String? filePath = data.photoPath; bool fileIsDownloading = !data.fileExists; + bool fileArchived = + data.attachment?.state == AttachmentState.archived.index; + + if (fileArchived) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Unavailable"), + const SizedBox(height: 8), + takePhotoButton + ], + ); + } if (fileIsDownloading) { return const Text("Downloading..."); diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index f18fb2a3..07a4b995 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -11,9 +11,21 @@ import 'package:powersync_flutter_demo/models/schema.dart'; late final PhotoAttachmentQueue attachmentQueue; final remoteStorage = SupabaseStorageAdapter(); +/// Function to handle errors when downloading attachments +/// Return false if you want to archive the attachment +Future onDownloadError(Attachment attachment, Object exception) async { + if (exception.toString().contains('Object not found')) { + return false; + } + return true; +} + class PhotoAttachmentQueue extends AbstractAttachmentQueue { PhotoAttachmentQueue(db, remoteStorage) - : super(db: db, remoteStorage: remoteStorage); + : super( + db: db, + remoteStorage: remoteStorage, + onDownloadError: onDownloadError); @override init() async { diff --git a/demos/supabase-todolist/lib/widgets/lists_page.dart b/demos/supabase-todolist/lib/widgets/lists_page.dart index 933fabc1..e31c2fc8 100644 --- a/demos/supabase-todolist/lib/widgets/lists_page.dart +++ b/demos/supabase-todolist/lib/widgets/lists_page.dart @@ -61,7 +61,7 @@ class _ListsWidgetState extends State { super.initState(); final stream = TodoList.watchListsWithStats(); _subscription = stream.listen((data) { - if (!mounted) { + if (!context.mounted) { return; } setState(() { diff --git a/demos/supabase-todolist/lib/widgets/login_page.dart b/demos/supabase-todolist/lib/widgets/login_page.dart index 9402cbbe..f54f09da 100644 --- a/demos/supabase-todolist/lib/widgets/login_page.dart +++ b/demos/supabase-todolist/lib/widgets/login_page.dart @@ -34,7 +34,7 @@ class _LoginPageState extends State { await Supabase.instance.client.auth.signInWithPassword( email: _usernameController.text, password: _passwordController.text); - if (mounted) { + if (context.mounted) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => listsPage, )); diff --git a/demos/supabase-todolist/lib/widgets/query_widget.dart b/demos/supabase-todolist/lib/widgets/query_widget.dart index 67786520..426c7060 100644 --- a/demos/supabase-todolist/lib/widgets/query_widget.dart +++ b/demos/supabase-todolist/lib/widgets/query_widget.dart @@ -46,7 +46,7 @@ class QueryWidgetState extends State { _subscription?.cancel(); final stream = db.watch(_query); _subscription = stream.listen((data) { - if (!mounted) { + if (!context.mounted) { return; } setState(() { diff --git a/demos/supabase-todolist/lib/widgets/signup_page.dart b/demos/supabase-todolist/lib/widgets/signup_page.dart index d2e4219e..2f9150b4 100644 --- a/demos/supabase-todolist/lib/widgets/signup_page.dart +++ b/demos/supabase-todolist/lib/widgets/signup_page.dart @@ -34,7 +34,7 @@ class _SignupPageState extends State { final response = await Supabase.instance.client.auth.signUp( email: _usernameController.text, password: _passwordController.text); - if (mounted) { + if (context.mounted) { if (response.session != null) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => homePage, diff --git a/demos/supabase-todolist/lib/widgets/todo_list_page.dart b/demos/supabase-todolist/lib/widgets/todo_list_page.dart index e8cebba5..e36e1867 100644 --- a/demos/supabase-todolist/lib/widgets/todo_list_page.dart +++ b/demos/supabase-todolist/lib/widgets/todo_list_page.dart @@ -62,7 +62,7 @@ class TodoListWidgetState extends State { super.initState(); final stream = widget.list.watchItems(); _subscription = stream.listen((data) { - if (!mounted) { + if (!context.mounted) { return; } setState(() { diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index d9ea70c7..773523c0 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.3+7" crypto: dependency: transitive description: @@ -220,10 +220,10 @@ packages: dependency: transitive description: name: gotrue - sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9 + sha256: "1bf6354278a98b8a1867263e94921da8a239de07e9babceab2b4e80af651a098" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" gtk: dependency: transitive description: @@ -420,17 +420,17 @@ packages: dependency: transitive description: name: postgrest - sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181" + sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.1" + version: "1.3.0-alpha.2" powersync_attachments_helper: dependency: "direct main" description: @@ -442,10 +442,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03" + sha256: "41d6c5e0327d6c270b98b79bfed672928244af60e2856770f3eff697f9efe459" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" retry: dependency: transitive description: @@ -506,10 +506,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: @@ -559,10 +559,10 @@ packages: dependency: "direct main" description: name: sqlite_async - sha256: "91f454cddc85617bea2c7c1544ff386887d0d2cf0ecdb3599015c05cc141ff4d" + sha256: "7a68036b2fc2fae5fc0efbc6bd436deb79e39090282348d979e6a7c38c8d067e" url: "https://pub.dev" source: hosted - version: "0.7.0-alpha.1" + version: "0.7.0-alpha.2" stack_trace: dependency: transitive description: @@ -607,18 +607,18 @@ packages: dependency: transitive description: name: supabase - sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40" + sha256: f431753d2a4cb9dacd72c7378154f806c2b2cef23859bd9cee1add23821e874d url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "5ef71289c380b6429216e941c69971c75eaab50d67fd7b540f6c1f6ebfc00ed7" + sha256: "30e966b89ee61dc9de845e2d7e1c60967b3189c410d105c6d42f09b6259f4cb6" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" term_glyph: dependency: transitive description: @@ -695,18 +695,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.0" url_launcher_windows: dependency: transitive description: diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index 1f538378..07e0d3a4 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -12,12 +12,12 @@ dependencies: sdk: flutter powersync_attachments_helper: ^0.3.0-alpha.1 - powersync: ^1.3.0-alpha.2 + powersync: ^1.3.0-alpha.3 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.7.0-alpha.1 + sqlite_async: ^0.7.0-alpha.2 camera: ^0.10.5+7 image: ^4.1.3 universal_io: ^2.2.2 diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index 4f75fe5a..a0f4fa85 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.0-alpha.3 + + - Fixed issue where disconnectAndClear would prevent subsequent sync connection on native platforms and would fail to clear the database on web. + ## 1.3.0-alpha.2 - **FIX**(powersync-attachements-helper): pubspec file (#29). diff --git a/packages/powersync/lib/src/database/powersync_db_mixin.dart b/packages/powersync/lib/src/database/powersync_db_mixin.dart index c178fe67..62dcb4da 100644 --- a/packages/powersync/lib/src/database/powersync_db_mixin.dart +++ b/packages/powersync/lib/src/database/powersync_db_mixin.dart @@ -44,6 +44,8 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { StreamController statusStreamController = StreamController.broadcast(); + @override + /// Broadcast stream that is notified of any table updates. /// /// Unlike in [SqliteDatabase.updates], the tables reported here are the @@ -125,7 +127,16 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { /// Use [connect] to connect again. Future disconnect() async { if (disconnecter != null) { - await disconnecter!.abort(); + /// Checking `disconnecter.aborted` prevents race conditions + /// where multiple calls to `disconnect` can attempt to abort + /// the controller more than once before it has finished aborting. + if (disconnecter!.aborted == false) { + await disconnecter!.abort(); + disconnecter = null; + } else { + /// Wait for the abort to complete. Continue updating the sync status after completed + await disconnecter!.onAbort; + } } setStatus( SyncStatus(connected: false, lastSyncedAt: currentStatus.lastSyncedAt)); diff --git a/packages/powersync/lib/src/database/web/web_powersync_database.dart b/packages/powersync/lib/src/database/web/web_powersync_database.dart index f6227b9a..e5639150 100644 --- a/packages/powersync/lib/src/database/web/web_powersync_database.dart +++ b/packages/powersync/lib/src/database/web/web_powersync_database.dart @@ -124,8 +124,7 @@ class PowerSyncDatabaseImpl // Disconnect if connected await disconnect(); - final disconnector = AbortController(); - disconnecter = disconnector; + disconnecter = AbortController(); await isInitialized; @@ -145,7 +144,7 @@ class PowerSyncDatabaseImpl sync.statusStream.listen((event) { setStatus(event); }); - sync.streamingSync(); + sync.streamingSync(abortController: disconnecter); } /// Takes a read lock, without starting a transaction. diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart index b470cb7a..6830534e 100644 --- a/packages/powersync/lib/src/streaming_sync.dart +++ b/packages/powersync/lib/src/streaming_sync.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert' as convert; import 'package:http/http.dart' as http; +import 'package:powersync/src/abort_controller.dart'; import 'package:powersync/src/exceptions.dart'; import 'package:powersync/src/log_internal.dart'; @@ -48,10 +49,14 @@ class StreamingSyncImplementation { statusStream = _statusStreamController.stream; } - Future streamingSync() async { + Future streamingSync({AbortController? abortController}) async { crudLoop(); var invalidCredentials = false; while (true) { + if (abortController?.aborted == true) { + abortController!.completeAbort(); + return; + } _updateStatus(connecting: true); try { if (invalidCredentials && invalidCredentialsCallback != null) { @@ -60,7 +65,7 @@ class StreamingSyncImplementation { await invalidCredentialsCallback!(); invalidCredentials = false; } - await streamingSyncIteration(); + await streamingSyncIteration(abortController: abortController); // Continue immediately } catch (e, stacktrace) { final message = _syncErrorMessage(e); @@ -173,7 +178,8 @@ class StreamingSyncImplementation { _statusStreamController.add(newStatus); } - Future streamingSyncIteration() async { + Future streamingSyncIteration( + {AbortController? abortController}) async { adapter.startSession(); final bucketEntries = await adapter.getBucketStates(); @@ -201,6 +207,9 @@ class StreamingSyncImplementation { bool haveInvalidated = false; await for (var line in merged) { + if (abortController?.aborted == true) { + return false; + } _updateStatus(connected: true, connecting: false); if (line is Checkpoint) { targetCheckpoint = line; diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index bc707dbb..45dd7e94 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync -version: 1.3.0-alpha.2 +version: 1.3.0-alpha.3 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. diff --git a/packages/powersync/test/disconnect_test.dart b/packages/powersync/test/disconnect_test.dart new file mode 100644 index 00000000..c4c7ad55 --- /dev/null +++ b/packages/powersync/test/disconnect_test.dart @@ -0,0 +1,60 @@ +import 'package:powersync/powersync.dart'; +import 'package:test/test.dart'; +import 'streaming_sync_test.dart'; +import 'utils/test_utils_impl.dart'; +import 'watch_test.dart'; + +final testUtils = TestUtils(); + +void main() { + group('Disconnect Tests', () { + late String path; + + setUp(() async { + path = testUtils.dbPath(); + await testUtils.cleanDb(path: path); + }); + + test('Multiple calls to disconnect', () async { + final db = await testUtils.setupPowerSync(path: path, schema: testSchema); + + credentialsCallback() async { + // A blank endpoint will fail, but that's okay for this test + final endpoint = ''; + return PowerSyncCredentials( + endpoint: endpoint, + token: 'token', + userId: 'u1', + expiresAt: DateTime.now()); + } + + db.retryDelay = Duration(milliseconds: 5000); + var connector = TestConnector(credentialsCallback); + await db.connect(connector: connector); + + // Call disconnect multiple times, each Future should resolve + final disconnect1 = db.disconnect(); + final disconnect2 = db.disconnect(); + + await expectLater(disconnect1, completes); + await expectLater(disconnect2, completes); + }); + + test('disconnectAndClear clears DB', () async { + final db = await testUtils.setupPowerSync(path: path, schema: testSchema); + + await db.execute( + 'INSERT INTO customers (id, name, email) VALUES(uuid(), ?, ?)', + ['Steven', 'steven@journeyapps.com']); + + final getCustomersQuery = 'SELECT * from customers'; + final initialCustomers = await db.getAll(getCustomersQuery); + expect(initialCustomers.length, equals(1)); + + await db.disconnectAndClear(); + + final finalCustomers = await db.getAll(getCustomersQuery); + expect(finalCustomers.length, equals(0)); + }); + }); +} diff --git a/packages/powersync_attachments_helper/CHANGELOG.md b/packages/powersync_attachments_helper/CHANGELOG.md index 95c3ac31..f0082638 100644 --- a/packages/powersync_attachments_helper/CHANGELOG.md +++ b/packages/powersync_attachments_helper/CHANGELOG.md @@ -2,10 +2,15 @@ - Added initial support for Web platform. +## 0.2.1 + +- Added `onUploadError` as an optional function that can be set when setting up the queue to handle upload errors +- Added `onDownloadError` as an optional function that can be set when setting up the queue to handle upload errors + ## 0.2.0 - Potentially BREAKING CHANGE for users who rely on multiple attachment queues. - Moved away from randomly generating queue table name in favour of a user creating a queue and table using a name of their choosing. + Moved away from randomly generating queue table name in favour of a user creating a queue and table using a name of their choosing. ## 0.1.5 diff --git a/packages/powersync_attachments_helper/lib/src/attachments_queue.dart b/packages/powersync_attachments_helper/lib/src/attachments_queue.dart index 8dcaa0a1..e9e525ab 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_queue.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_queue.dart @@ -24,16 +24,28 @@ abstract class AbstractAttachmentQueue { final LocalStorageAdapter localStorage = LocalStorageAdapter(); String attachmentsQueueTableName; + /// Function to handle errors when downloading attachments + /// Return true if you want to ignore attachment + Future Function(Attachment attachment, Object exception)? + onDownloadError; + + /// Function to handle errors when uploading attachments + /// Return true if you want to ignore attachment + Future Function(Attachment attachment, Object exception)? onUploadError; + AbstractAttachmentQueue( {required this.db, required this.remoteStorage, this.attachmentDirectoryName = 'attachments', this.attachmentsQueueTableName = defaultAttachmentsQueueTableName, + this.onDownloadError, + this.onUploadError, performInitialSync = true}) { attachmentsService = AttachmentsService( db, localStorage, attachmentDirectoryName, attachmentsQueueTableName); syncingService = SyncingService( - db, remoteStorage, localStorage, attachmentsService, getLocalUri); + db, remoteStorage, localStorage, attachmentsService, getLocalUri, + onDownloadError: onDownloadError, onUploadError: onUploadError); } /// Create watcher to get list of ID's from a table to be used for syncing in the attachment queue. diff --git a/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart b/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart index 966d115a..91e940b5 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart @@ -64,11 +64,8 @@ class Attachment { /// 1. Attachment to be uploaded /// 2. Attachment to be downloaded /// 3. Attachment to be deleted -enum AttachmentState { - queuedUpload, - queuedDownload, - queuedDelete, -} +/// 4. Attachment to be archived +enum AttachmentState { queuedUpload, queuedDownload, queuedDelete, archived } class AttachmentsQueueTable extends Table { AttachmentsQueueTable( diff --git a/packages/powersync_attachments_helper/lib/src/attachments_service.dart b/packages/powersync_attachments_helper/lib/src/attachments_service.dart index da67244a..f8ac8d9c 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_service.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_service.dart @@ -23,6 +23,11 @@ class AttachmentsService { Future deleteAttachment(String id) async => db.execute('DELETE FROM $table WHERE id = ?', [id]); + ///Set the state of the attachment to ignore. + Future ignoreAttachment(String id) async => db.execute( + 'UPDATE $table SET state = ${AttachmentState.archived.index} WHERE id = ?', + [id]); + /// Get the attachment from the attachment queue using an ID. Future getAttachment(String id) async => db.getOptional('SELECT * FROM $table WHERE id = ?', [id]).then((row) { diff --git a/packages/powersync_attachments_helper/lib/src/syncing_service.dart b/packages/powersync_attachments_helper/lib/src/syncing_service.dart index 595b4643..853a1504 100644 --- a/packages/powersync_attachments_helper/lib/src/syncing_service.dart +++ b/packages/powersync_attachments_helper/lib/src/syncing_service.dart @@ -15,13 +15,18 @@ class SyncingService { final LocalStorageAdapter localStorage; final AttachmentsService attachmentsService; final Function getLocalUri; + final Future Function(Attachment attachment, Object exception)? + onDownloadError; + final Future Function(Attachment attachment, Object exception)? + onUploadError; SyncingService(this.db, this.remoteStorage, this.localStorage, - this.attachmentsService, this.getLocalUri); + this.attachmentsService, this.getLocalUri, + {this.onDownloadError, this.onUploadError}); /// Upload attachment from local storage and to remote storage /// then remove it from the queue. - /// If duplicate of the file is found uploading is ignored and + /// If duplicate of the file is found uploading is archived and /// the attachment is removed from the queue. Future uploadAttachment(Attachment attachment) async { if (attachment.localUri == null) { @@ -44,6 +49,14 @@ class SyncingService { } log.severe('Upload attachment error for attachment $attachment', e); + if (onUploadError != null) { + bool shouldRetry = await onUploadError!(attachment, e); + if (!shouldRetry) { + log.info('Attachment with ID ${attachment.id} has been archived', e); + await attachmentsService.ignoreAttachment(attachment.id); + } + } + return; } } @@ -63,6 +76,15 @@ class SyncingService { await attachmentsService.deleteAttachment(attachment.id); return; } catch (e) { + if (onDownloadError != null) { + bool shouldRetry = await onDownloadError!(attachment, e); + if (!shouldRetry) { + log.info('Attachment with ID ${attachment.id} has been archived', e); + await attachmentsService.ignoreAttachment(attachment.id); + return; + } + } + log.severe('Download attachment error for attachment $attachment', e); return; } diff --git a/packages/powersync_attachments_helper/pubspec.yaml b/packages/powersync_attachments_helper/pubspec.yaml index 0df84e18..38610843 100644 --- a/packages/powersync_attachments_helper/pubspec.yaml +++ b/packages/powersync_attachments_helper/pubspec.yaml @@ -10,10 +10,10 @@ dependencies: flutter: sdk: flutter - powersync: ^1.3.0-alpha.2 + powersync: ^1.3.0-alpha.3 logging: ^1.2.0 sqlite3: ^2.3.0 - sqlite_async: ^0.7.0-alpha.1 + sqlite_async: ^0.7.0-alpha.2 path_provider: ^2.1.1 dev_dependencies: diff --git a/pubspec.lock b/pubspec.lock index b7baa1d7..99f242bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: