diff --git a/CHANGELOG.md b/CHANGELOG.md index cd539f70..1836be21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,40 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2025-01-28 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.1.2`](#powersync_core---v112) + - [`powersync_attachments_helper` - `v0.6.18`](#powersync_attachments_helper---v0618) + - [`powersync_sqlcipher` - `v0.1.4`](#powersync_sqlcipher---v014) + - [`powersync` - `v1.11.2`](#powersync---v1112) + +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.6.18` + - `powersync_sqlcipher` - `v0.1.4` + - `powersync` - `v1.11.2` + +--- + +#### `powersync_core` - `v1.1.2` + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + + ## 2025-01-16 ### Changes diff --git a/demos/benchmarks/pubspec.yaml b/demos/benchmarks/pubspec.yaml index 8af7e01e..ef22b44e 100644 --- a/demos/benchmarks/pubspec.yaml +++ b/demos/benchmarks/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/django-todolist/pubspec.yaml b/demos/django-todolist/pubspec.yaml index 7c44e523..f04f5ace 100644 --- a/demos/django-todolist/pubspec.yaml +++ b/demos/django-todolist/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/firebase-nodejs-todolist/pubspec.yaml b/demos/firebase-nodejs-todolist/pubspec.yaml index 4e0efe45..d4f22476 100644 --- a/demos/firebase-nodejs-todolist/pubspec.yaml +++ b/demos/firebase-nodejs-todolist/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 diff --git a/demos/supabase-anonymous-auth/pubspec.yaml b/demos/supabase-anonymous-auth/pubspec.yaml index b8da34bd..e0601fde 100644 --- a/demos/supabase-anonymous-auth/pubspec.yaml +++ b/demos/supabase-anonymous-auth/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 diff --git a/demos/supabase-edge-function-auth/pubspec.yaml b/demos/supabase-edge-function-auth/pubspec.yaml index 109220f0..410d316b 100644 --- a/demos/supabase-edge-function-auth/pubspec.yaml +++ b/demos/supabase-edge-function-auth/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 diff --git a/demos/supabase-simple-chat/pubspec.yaml b/demos/supabase-simple-chat/pubspec.yaml index 42ed12aa..169b476b 100644 --- a/demos/supabase-simple-chat/pubspec.yaml +++ b/demos/supabase-simple-chat/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: supabase_flutter: ^2.0.2 timeago: ^3.6.0 - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/supabase-todolist-drift/pubspec.yaml b/demos/supabase-todolist-drift/pubspec.yaml index 53898117..317d60c1 100644 --- a/demos/supabase-todolist-drift/pubspec.yaml +++ b/demos/supabase-todolist-drift/pubspec.yaml @@ -9,8 +9,8 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.6.17 - powersync: ^1.11.1 + powersync_attachments_helper: ^0.6.17+1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 diff --git a/demos/supabase-todolist-optional-sync/pubspec.yaml b/demos/supabase-todolist-optional-sync/pubspec.yaml index 579d66e0..318c750a 100644 --- a/demos/supabase-todolist-optional-sync/pubspec.yaml +++ b/demos/supabase-todolist-optional-sync/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.11.1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 diff --git a/demos/supabase-todolist/.gitignore b/demos/supabase-todolist/.gitignore index 1a825b5b..0f3655d3 100644 --- a/demos/supabase-todolist/.gitignore +++ b/demos/supabase-todolist/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-todolist/macos/Podfile.lock b/demos/supabase-todolist/macos/Podfile.lock index cd6e215f..28fcb6c4 100644 --- a/demos/supabase-todolist/macos/Podfile.lock +++ b/demos/supabase-todolist/macos/Podfile.lock @@ -5,10 +5,10 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.3.4) + - powersync-sqlite-core (0.3.9) - powersync_flutter_libs (0.0.1): - FlutterMacOS - - powersync-sqlite-core (~> 0.3.4) + - powersync-sqlite-core (~> 0.3.8) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -64,16 +64,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: d029aa444d33acbb05b47f9f9757b2650578e2d3 - powersync_flutter_libs: 44829eda70d4f87c9271e963a54126ce19408d7c - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 7515d321eb8e3c08b5259cdadb9d19b1876fe13a + powersync_flutter_libs: 330d8309223a121ec15a7334d9edc105053e5f82 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + sqlite3_flutter_libs: 03311aede9d32fb2d24e32bebb8cd01c3b2e6239 + url_launcher_macos: de10e46d8d8b9e3a7b8a133e8de92b104379f05e PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist/macos/Runner/AppDelegate.swift b/demos/supabase-todolist/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/demos/supabase-todolist/macos/Runner/AppDelegate.swift +++ b/demos/supabase-todolist/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index fb668184..a5a26983 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.6.17 - powersync: ^1.11.1 + powersync_attachments_helper: ^0.6.17+1 + powersync: ^1.11.2 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index b7b7294b..c65427a7 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.11.2 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + ## 1.11.1 - Fix `statusStream` emitting the same sync status multiple times. diff --git a/packages/powersync/bin/setup_web.dart b/packages/powersync/bin/setup_web.dart index 6d29a6be..dd4042c6 100644 --- a/packages/powersync/bin/setup_web.dart +++ b/packages/powersync/bin/setup_web.dart @@ -1,176 +1,4 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:args/args.dart'; +// ignore: implementation_imports +import 'package:powersync_core/src/setup_web.dart'; -void main(List arguments) async { - var parser = ArgParser(); - // Add a flag to enable/disable the download of worker (defaults to true) - // Pass the --no-worker argument to disable the download of the worker - // dart run powersync:setup_web --no-worker - parser.addFlag('worker', defaultsTo: true); - // Add a option to specify the output directory (defaults to web) - // Pass the --output-dir argument to specify the output directory - // dart run powersync:setup_web --output-dir assets - parser.addOption('output-dir', abbr: 'o', defaultsTo: 'web'); - var results = parser.parse(arguments); - bool downloadWorker = results.flag('worker'); - String outputDir = results.option('output-dir')!; - - final root = Directory.current.uri; - print('Project root: ${root.toFilePath()}'); - - final wasmPath = '${root.toFilePath()}$outputDir/sqlite3.wasm'; - - final workerPath = '${root.toFilePath()}$outputDir/powersync_db.worker.js'; - final syncWorkerPath = - '${root.toFilePath()}$outputDir/powersync_sync.worker.js'; - - final packageConfigFile = File.fromUri( - root.resolve('.dart_tool/package_config.json'), - ); - dynamic packageConfig; - try { - packageConfig = json.decode(await packageConfigFile.readAsString()); - } on FileSystemException { - print('Missing .dart_tool/package_config.json'); - print('Run `flutter pub get` first.'); - exit(1); - } on FormatException { - print('Invalid .dart_tool/package_config.json'); - print('Run `flutter pub get` first.'); - exit(1); - } - - try { - final httpClient = HttpClient(); - - final powersyncPackageName = 'powersync'; - - if (downloadWorker) { - final powersyncPkg = - getPackageFromConfig(packageConfig, powersyncPackageName); - - final powersyncVersion = getPubspecVersion( - packageConfigFile, powersyncPkg, powersyncPackageName); - - final workerUrl = - 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_db.worker.js'; - - final syncWorkerUrl = - 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_sync.worker.js'; - - await downloadFile(httpClient, workerUrl, workerPath); - await downloadFile(httpClient, syncWorkerUrl, syncWorkerPath); - } - - final sqlitePackageName = 'sqlite3'; - - final sqlite3Pkg = getPackageFromConfig(packageConfig, sqlitePackageName); - - String sqlite3Version = - "v${getPubspecVersion(packageConfigFile, sqlite3Pkg, sqlitePackageName)}"; - - List tags = await getLatestTagsFromRelease(httpClient); - String? matchTag = tags.firstWhereOrNull((element) => - element.contains(sqlite3Version) && coreVersionIsInRange(element)); - if (matchTag != null) { - sqlite3Version = matchTag; - } else { - throw Exception( - """No compatible powersync core version found for sqlite3 version $sqlite3Version - Latest supported sqlite3 versions: ${tags.take(3).map((tag) => tag.split('-')[0]).join(', ')}. - You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases"""); - } - - final sqliteUrl = - 'https://github.com/powersync-ja/sqlite3.dart/releases/download/$sqlite3Version/sqlite3.wasm'; - - await downloadFile(httpClient, sqliteUrl, wasmPath); - } catch (e) { - print(e); - exit(1); - } -} - -bool coreVersionIsInRange(String tag) { - // Sets the range of powersync core version that is compatible with the sqlite3 version - // We're a little more selective in the versions chosen here than the range - // we're compatible with. - VersionConstraint constraint = VersionConstraint.parse('>=0.3.0 <0.4.0'); - List parts = tag.split('-'); - String powersyncPart = parts[1]; - - List versionParts = powersyncPart.split('.'); - String extractedVersion = - versionParts.sublist(versionParts.length - 3).join('.'); - final coreVersion = Version.parse(extractedVersion); - if (constraint.allows(coreVersion)) { - return true; - } - return false; -} - -dynamic getPackageFromConfig(dynamic packageConfig, String packageName) { - final pkg = (packageConfig['packages'] ?? []).firstWhere( - (e) => e['name'] == packageName, - orElse: () => null, - ); - if (pkg == null) { - throw Exception('Dependency on package:$packageName is required'); - } - return pkg; -} - -String getPubspecVersion( - File packageConfigFile, dynamic package, String packageName) { - final rootUri = packageConfigFile.uri.resolve(package['rootUri'] ?? ''); - print('Using package:$packageName from ${rootUri.toFilePath()}'); - - String pubspec = - File('${rootUri.toFilePath()}/pubspec.yaml').readAsStringSync(); - Pubspec parsed = Pubspec.parse(pubspec); - final version = parsed.version?.toString(); - if (version == null) { - throw Exception( - "${capitalize(packageName)} version not found. Run `flutter pub get` first."); - } - return version; -} - -String capitalize(String s) => s[0].toUpperCase() + s.substring(1); - -Future> getLatestTagsFromRelease(HttpClient httpClient) async { - var request = await httpClient.getUrl(Uri.parse( - "https://api.github.com/repos/powersync-ja/sqlite3.dart/releases")); - var response = await request.close(); - if (response.statusCode == HttpStatus.ok) { - var res = await response.transform(utf8.decoder).join(); - List jsonObj = json.decode(res); - List tags = []; - for (dynamic obj in jsonObj) { - final tagName = obj['tag_name'] as String; - if (!tagName.contains("-powersync")) continue; - tags.add(tagName); - } - return tags; - } else { - throw Exception("Failed to fetch GitHub releases and tags"); - } -} - -Future downloadFile( - HttpClient httpClient, String url, String savePath) async { - print('Downloading: $url'); - var request = await httpClient.getUrl(Uri.parse(url)); - var response = await request.close(); - if (response.statusCode == HttpStatus.ok) { - var file = File(savePath); - await response.pipe(file.openWrite()); - } else { - throw Exception( - 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); - } -} +void main(List args) => downloadWebAssets(args); diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index d3c2625c..5d82bd1a 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync -version: 1.11.1 +version: 1.11.2 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Flutter SDK - sync engine for building local-first apps. @@ -12,12 +12,9 @@ dependencies: sdk: flutter sqlite3_flutter_libs: ^0.5.23 - powersync_core: ^1.1.1 + powersync_core: ^1.1.2 powersync_flutter_libs: ^0.4.4 collection: ^1.17.0 - pubspec_parse: ^1.3.0 - args: ^2.5.0 - pub_semver: ^2.1.4 dev_dependencies: lints: ^5.1.0 diff --git a/packages/powersync_attachments_helper/CHANGELOG.md b/packages/powersync_attachments_helper/CHANGELOG.md index b86e6e45..e94f713f 100644 --- a/packages/powersync_attachments_helper/CHANGELOG.md +++ b/packages/powersync_attachments_helper/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.18 + + - Update a dependency to the latest release. + ## 0.6.17 - **FEAT**: PowerSync encryption with SQLCipher ([#194](https://github.com/powersync-ja/powersync.dart/issues/194)). ([ea6186d7](https://github.com/powersync-ja/powersync.dart/commit/ea6186d7d844d976fafb0c2e2e8a9f25e4deb08c)) diff --git a/packages/powersync_attachments_helper/pubspec.yaml b/packages/powersync_attachments_helper/pubspec.yaml index 943c7941..a9bbe07f 100644 --- a/packages/powersync_attachments_helper/pubspec.yaml +++ b/packages/powersync_attachments_helper/pubspec.yaml @@ -1,6 +1,6 @@ name: powersync_attachments_helper description: A helper library for handling attachments when using PowerSync. -version: 0.6.17 +version: 0.6.18 repository: https://github.com/powersync-ja/powersync.dart homepage: https://www.powersync.com/ environment: @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter - powersync_core: ^1.1.0 + powersync_core: ^1.1.2 logging: ^1.2.0 sqlite_async: ^0.11.0 path_provider: ^2.0.13 diff --git a/packages/powersync_core/CHANGELOG.md b/packages/powersync_core/CHANGELOG.md index 3fcf080f..c2535002 100644 --- a/packages/powersync_core/CHANGELOG.md +++ b/packages/powersync_core/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.2 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + ## 1.1.1 - Fix `statusStream` emitting the same sync status multiple times. diff --git a/packages/powersync_core/lib/src/database/powersync_database_impl.dart b/packages/powersync_core/lib/src/database/powersync_database_impl.dart index 7c98975f..7a439ef0 100644 --- a/packages/powersync_core/lib/src/database/powersync_database_impl.dart +++ b/packages/powersync_core/lib/src/database/powersync_database_impl.dart @@ -6,4 +6,4 @@ export 'powersync_database_impl_stub.dart' // ignore: uri_does_not_exist if (dart.library.io) './native/native_powersync_database.dart' // ignore: uri_does_not_exist - if (dart.library.html) './web/web_powersync_database.dart'; + if (dart.library.js_interop) './web/web_powersync_database.dart'; diff --git a/packages/powersync_core/lib/src/open_factory.dart b/packages/powersync_core/lib/src/open_factory.dart index c32daf83..25a14643 100644 --- a/packages/powersync_core/lib/src/open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory.dart @@ -2,8 +2,8 @@ // To conditionally export an implementation for either web or "native" platforms // The sqlite library uses dart:ffi which is not supported on web -export './open_factory/open_factory_stub.dart' +export 'open_factory/open_factory_stub.dart' // ignore: uri_does_not_exist - if (dart.library.io) './open_factory/native/native_open_factory.dart' + if (dart.library.io) 'open_factory/native/native_open_factory.dart' // ignore: uri_does_not_exist - if (dart.library.html) './open_factory/web/web_open_factory.dart'; + if (dart.library.js_interop) 'open_factory/web/web_open_factory.dart'; diff --git a/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart b/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart index a6dfae51..6f5f156f 100644 --- a/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart @@ -11,7 +11,7 @@ import '../../web/worker_utils.dart'; /// Web implementation for [AbstractPowerSyncOpenFactory] class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory - implements WebSqliteOpenFactory { + with WebSqliteOpenFactory { PowerSyncOpenFactory({ required super.path, super.sqliteOptions, @@ -26,6 +26,12 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory ); } + @override + Future connectToWorker( + WebSqlite sqlite, String name) { + return sqlite.connectToRecommended(name); + } + @override void enableExtension() { // No op for web @@ -34,11 +40,11 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory @override Future openConnection(SqliteOpenOptions options) async { var conn = await super.openConnection(options); - for (final statement in super.pragmaStatements(options)) { + for (final statement in pragmaStatements(options)) { await conn.execute(statement); } - return super.openConnection(options); + return conn; } @override diff --git a/packages/powersync_core/lib/src/setup_web.dart b/packages/powersync_core/lib/src/setup_web.dart new file mode 100644 index 00000000..6e62f980 --- /dev/null +++ b/packages/powersync_core/lib/src/setup_web.dart @@ -0,0 +1,178 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:args/args.dart'; + +Future downloadWebAssets(List arguments, + {bool encryption = false}) async { + var parser = ArgParser(); + // Add a flag to enable/disable the download of worker (defaults to true) + // Pass the --no-worker argument to disable the download of the worker + // dart run powersync:setup_web --no-worker + parser.addFlag('worker', defaultsTo: true); + // Add a option to specify the output directory (defaults to web) + // Pass the --output-dir argument to specify the output directory + // dart run powersync:setup_web --output-dir assets + parser.addOption('output-dir', abbr: 'o', defaultsTo: 'web'); + var results = parser.parse(arguments); + bool downloadWorker = results.flag('worker'); + String outputDir = results.option('output-dir')!; + + final root = Directory.current.uri; + print('Project root: ${root.toFilePath()}'); + + final wasmFileName = encryption ? 'sqlite3mc.wasm' : 'sqlite3.wasm'; + final wasmPath = '${root.toFilePath()}$outputDir/$wasmFileName'; + + final workerPath = '${root.toFilePath()}$outputDir/powersync_db.worker.js'; + final syncWorkerPath = + '${root.toFilePath()}$outputDir/powersync_sync.worker.js'; + + final packageConfigFile = File.fromUri( + root.resolve('.dart_tool/package_config.json'), + ); + dynamic packageConfig; + try { + packageConfig = json.decode(await packageConfigFile.readAsString()); + } on FileSystemException { + print('Missing .dart_tool/package_config.json'); + print('Run `flutter pub get` first.'); + exit(1); + } on FormatException { + print('Invalid .dart_tool/package_config.json'); + print('Run `flutter pub get` first.'); + exit(1); + } + + try { + final httpClient = HttpClient(); + + final powersyncPackageName = 'powersync'; + + if (downloadWorker) { + final powersyncPkg = + getPackageFromConfig(packageConfig, powersyncPackageName); + + final powersyncVersion = getPubspecVersion( + packageConfigFile, powersyncPkg, powersyncPackageName); + + final workerUrl = + 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_db.worker.js'; + + final syncWorkerUrl = + 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_sync.worker.js'; + + await downloadFile(httpClient, workerUrl, workerPath); + await downloadFile(httpClient, syncWorkerUrl, syncWorkerPath); + } + + final sqlitePackageName = 'sqlite3'; + + final sqlite3Pkg = getPackageFromConfig(packageConfig, sqlitePackageName); + + String sqlite3Version = + "v${getPubspecVersion(packageConfigFile, sqlite3Pkg, sqlitePackageName)}"; + + List tags = await getLatestTagsFromRelease(httpClient); + String? matchTag = tags.firstWhereOrNull((element) => + element.contains(sqlite3Version) && coreVersionIsInRange(element)); + if (matchTag != null) { + sqlite3Version = matchTag; + } else { + throw Exception( + """No compatible powersync core version found for sqlite3 version $sqlite3Version + Latest supported sqlite3 versions: ${tags.take(3).map((tag) => tag.split('-')[0]).join(', ')}. + You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases"""); + } + + final sqliteUrl = + 'https://github.com/powersync-ja/sqlite3.dart/releases/download/$sqlite3Version/$wasmFileName'; + + await downloadFile(httpClient, sqliteUrl, wasmPath); + } catch (e) { + print(e); + exit(1); + } +} + +bool coreVersionIsInRange(String tag) { + // Sets the range of powersync core version that is compatible with the sqlite3 version + // We're a little more selective in the versions chosen here than the range + // we're compatible with. + VersionConstraint constraint = VersionConstraint.parse('>=0.3.0 <0.4.0'); + List parts = tag.split('-'); + String powersyncPart = parts[1]; + + List versionParts = powersyncPart.split('.'); + String extractedVersion = + versionParts.sublist(versionParts.length - 3).join('.'); + final coreVersion = Version.parse(extractedVersion); + if (constraint.allows(coreVersion)) { + return true; + } + return false; +} + +dynamic getPackageFromConfig(dynamic packageConfig, String packageName) { + final pkg = (packageConfig['packages'] ?? []).firstWhere( + (e) => e['name'] == packageName, + orElse: () => null, + ); + if (pkg == null) { + throw Exception('Dependency on package:$packageName is required'); + } + return pkg; +} + +String getPubspecVersion( + File packageConfigFile, dynamic package, String packageName) { + final rootUri = packageConfigFile.uri.resolve(package['rootUri'] ?? ''); + print('Using package:$packageName from ${rootUri.toFilePath()}'); + + String pubspec = + File('${rootUri.toFilePath()}/pubspec.yaml').readAsStringSync(); + Pubspec parsed = Pubspec.parse(pubspec); + final version = parsed.version?.toString(); + if (version == null) { + throw Exception( + "${capitalize(packageName)} version not found. Run `flutter pub get` first."); + } + return version; +} + +String capitalize(String s) => s[0].toUpperCase() + s.substring(1); + +Future> getLatestTagsFromRelease(HttpClient httpClient) async { + var request = await httpClient.getUrl(Uri.parse( + "https://api.github.com/repos/powersync-ja/sqlite3.dart/releases")); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var res = await response.transform(utf8.decoder).join(); + List jsonObj = json.decode(res); + List tags = []; + for (dynamic obj in jsonObj) { + final tagName = obj['tag_name'] as String; + if (!tagName.contains("-powersync")) continue; + tags.add(tagName); + } + return tags; + } else { + throw Exception("Failed to fetch GitHub releases and tags"); + } +} + +Future downloadFile( + HttpClient httpClient, String url, String savePath) async { + print('Downloading: $url'); + var request = await httpClient.getUrl(Uri.parse(url)); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var file = File(savePath); + await response.pipe(file.openWrite()); + } else { + throw Exception( + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + } +} diff --git a/packages/powersync_core/lib/src/user_agent/user_agent.dart b/packages/powersync_core/lib/src/user_agent/user_agent.dart index 8f5405e1..ffcae2bd 100644 --- a/packages/powersync_core/lib/src/user_agent/user_agent.dart +++ b/packages/powersync_core/lib/src/user_agent/user_agent.dart @@ -2,4 +2,4 @@ export './user_agent_stub.dart' // ignore: uri_does_not_exist if (dart.library.io) './user_agent_native.dart' // ignore: uri_does_not_exist - if (dart.library.html) './user_agent_web.dart'; + if (dart.library.js_interop) './user_agent_web.dart'; diff --git a/packages/powersync_core/lib/src/version.dart b/packages/powersync_core/lib/src/version.dart index 5333794a..32d95ac0 100644 --- a/packages/powersync_core/lib/src/version.dart +++ b/packages/powersync_core/lib/src/version.dart @@ -1 +1 @@ -const String libraryVersion = '1.1.1'; +const String libraryVersion = '1.1.2'; diff --git a/packages/powersync_core/lib/src/web/worker_utils.dart b/packages/powersync_core/lib/src/web/worker_utils.dart index f2a5ac9a..6f010219 100644 --- a/packages/powersync_core/lib/src/web/worker_utils.dart +++ b/packages/powersync_core/lib/src/web/worker_utils.dart @@ -8,13 +8,27 @@ import 'package:uuid/uuid.dart'; final class PowerSyncAsyncSqliteController extends AsyncSqliteController { @override - Future openDatabase( - WasmSqlite3 sqlite3, String path, String vfs) async { - final asyncDb = await super.openDatabase(sqlite3, path, vfs); + Future openDatabase(WasmSqlite3 sqlite3, String path, + String vfs, JSAny? additionalData) async { + final asyncDb = + await super.openDatabase(sqlite3, path, vfs, additionalData); setupPowerSyncDatabase(asyncDb.database); return asyncDb; } + @override + CommonDatabase openUnderlying( + WasmSqlite3 sqlite3, String path, String vfs, JSAny? additionalData) { + final options = additionalData == null + ? null + : additionalData as PowerSyncAdditionalOpenOptions; + if (options != null && options.useMultipleCiphersVfs) { + vfs = 'multipleciphers-$vfs'; + } + + return sqlite3.open(path, vfs: vfs); + } + @override Future handleCustomRequest( ClientConnection connection, JSAny? request) { @@ -22,6 +36,17 @@ final class PowerSyncAsyncSqliteController extends AsyncSqliteController { } } +@JS() +@anonymous +extension type PowerSyncAdditionalOpenOptions._(JSObject _) + implements JSObject { + external factory PowerSyncAdditionalOpenOptions({ + required bool useMultipleCiphersVfs, + }); + + external bool get useMultipleCiphersVfs; +} + // Registers custom SQLite functions for the SQLite connection void setupPowerSyncDatabase(CommonDatabase database) { setupCommonDBFunctions(database); diff --git a/packages/powersync_core/lib/web.dart b/packages/powersync_core/lib/web.dart new file mode 100644 index 00000000..fbf8598e --- /dev/null +++ b/packages/powersync_core/lib/web.dart @@ -0,0 +1,19 @@ +/// Internal options used to customize how PowerSync opens databases on the web. +library; + +export 'src/web/worker_utils.dart' show PowerSyncAdditionalOpenOptions; +export 'package:sqlite_async/sqlite3_web.dart'; +export 'package:sqlite_async/web.dart'; + +import 'package:sqlite_async/web.dart'; +import 'powersync_core.dart' as core; +import 'src/open_factory/web/web_open_factory.dart'; + +/// The default [core.PowerSyncOpenFactory] implementation for the web. Unlike +/// the cross-platform interface, this is guaranteed to implement +/// [WebSqliteOpenFactory]. +/// +/// This typedef is mostly used internally, e.g. in the web implementation of +/// `powersync_sqlcipher` which relies on the fact that web-specific factory +/// methods are available. +typedef PowerSyncWebOpenFactory = PowerSyncOpenFactory; diff --git a/packages/powersync_core/pubspec.yaml b/packages/powersync_core/pubspec.yaml index be3f8595..f1c1cf29 100644 --- a/packages/powersync_core/pubspec.yaml +++ b/packages/powersync_core/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync_core -version: 1.1.1 +version: 1.1.2 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Dart SDK - sync engine for building local-first apps. @@ -8,10 +8,12 @@ environment: sdk: ^3.4.3 dependencies: - sqlite_async: ^0.11.1 + sqlite_async: ^0.11.2 # We only use sqlite3 as a transitive dependency, # but right now we need a minimum of v2.4.6. sqlite3: ^2.4.6 + # We implement a database controller, which is an interface of sqlite3_web. + sqlite3_web: ^0.3.0 universal_io: ^2.0.0 meta: ^1.0.0 http: ^1.1.0 @@ -22,6 +24,11 @@ dependencies: fetch_client: ^1.1.2 web: ^1.0.0 + # Only used internally to download WASM / worker files. + args: ^2.6.0 + pub_semver: ^2.0.0 + pubspec_parse: ^1.3.0 + dev_dependencies: lints: ^5.1.1 test: ^1.25.0 diff --git a/packages/powersync_core/test/utils/test_utils_impl.dart b/packages/powersync_core/test/utils/test_utils_impl.dart index 99a34d39..3406d1ec 100644 --- a/packages/powersync_core/test/utils/test_utils_impl.dart +++ b/packages/powersync_core/test/utils/test_utils_impl.dart @@ -2,4 +2,4 @@ export 'stub_test_utils.dart' // ignore: uri_does_not_exist if (dart.library.io) 'native_test_utils.dart' // ignore: uri_does_not_exist - if (dart.library.html) 'web_test_utils.dart'; + if (dart.library.js_interop) 'web_test_utils.dart'; diff --git a/packages/powersync_sqlcipher/CHANGELOG.md b/packages/powersync_sqlcipher/CHANGELOG.md index 1d6b5a5b..9a82e548 100644 --- a/packages/powersync_sqlcipher/CHANGELOG.md +++ b/packages/powersync_sqlcipher/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.4 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + ## 0.1.3 - Fix `statusStream` emitting the same sync status multiple times. diff --git a/packages/powersync_sqlcipher/README.md b/packages/powersync_sqlcipher/README.md index c9020679..70891a33 100644 --- a/packages/powersync_sqlcipher/README.md +++ b/packages/powersync_sqlcipher/README.md @@ -19,6 +19,11 @@ flutter pub add powersync_sqlcipher ``` Version history can be found [here](https://pub.dev/packages/powersync_sqlcipher/versions). +To use this package on the web, additional assets are required. You can download them by running this in your project directory: + +``` +dart run powersync_sqlcipher:setup_web +``` ### Usage diff --git a/packages/powersync_sqlcipher/bin/setup_web.dart b/packages/powersync_sqlcipher/bin/setup_web.dart new file mode 100644 index 00000000..b3509e02 --- /dev/null +++ b/packages/powersync_sqlcipher/bin/setup_web.dart @@ -0,0 +1,4 @@ +// ignore: implementation_imports +import 'package:powersync_core/src/setup_web.dart'; + +void main(List args) => downloadWebAssets(args, encryption: true); diff --git a/packages/powersync_sqlcipher/lib/powersync.dart b/packages/powersync_sqlcipher/lib/powersync.dart index edb605f2..2de08e5f 100644 --- a/packages/powersync_sqlcipher/lib/powersync.dart +++ b/packages/powersync_sqlcipher/lib/powersync.dart @@ -3,5 +3,30 @@ /// Use [PowerSyncSQLCipherOpenFactory] to open an encrypted database. library; -export 'src/sqlcipher.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_sqlcipher/powersync.dart'; + export 'package:powersync_core/powersync_core.dart'; + +import 'src/stub.dart' + if (dart.library.js_interop) 'src/web_encryption.dart' + if (dart.library.ffi) 'src/sqlcipher.dart'; + +const _defaultOptions = SqliteOptions( + webSqliteOptions: WebSqliteOptions( + wasmUri: 'sqlite3mc.wasm', workerUri: 'powersync_db.worker.js'), +); + +/// A factory for opening a database with SQLCipher encryption. +/// An encryption [key] is required to open the database. +abstract interface class PowerSyncSQLCipherOpenFactory + extends PowerSyncOpenFactory { + factory PowerSyncSQLCipherOpenFactory( + {required String path, + required String key, + SqliteOptions sqliteOptions = _defaultOptions}) { + return cipherFactory(path: path, key: key, options: sqliteOptions); + } + + String get key; +} diff --git a/packages/powersync_sqlcipher/lib/src/sqlcipher.dart b/packages/powersync_sqlcipher/lib/src/sqlcipher.dart index b4bc8af0..3e26cfac 100644 --- a/packages/powersync_sqlcipher/lib/src/sqlcipher.dart +++ b/packages/powersync_sqlcipher/lib/src/sqlcipher.dart @@ -1,17 +1,21 @@ -import 'package:powersync_core/powersync_core.dart'; import 'package:powersync_core/sqlite3_common.dart'; import 'package:powersync_core/sqlite3_open.dart' as sqlite3_open; import 'package:powersync_core/sqlite_async.dart'; import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart'; -/// A factory for opening a database with SQLCipher encryption. -/// An encryption [key] is required to open the database. -class PowerSyncSQLCipherOpenFactory extends PowerSyncOpenFactory { - PowerSyncSQLCipherOpenFactory( - {required super.path, required this.key, super.sqliteOptions}); +import '../powersync.dart'; +final class _NativeCipherOpenFactory extends PowerSyncOpenFactory + implements PowerSyncSQLCipherOpenFactory { + @override final String key; + _NativeCipherOpenFactory({ + required super.path, + required this.key, + super.sqliteOptions, + }); + @override List pragmaStatements(SqliteOpenOptions options) { final basePragmaStatements = super.pragmaStatements(options); @@ -37,3 +41,11 @@ class PowerSyncSQLCipherOpenFactory extends PowerSyncOpenFactory { return db; } } + +PowerSyncSQLCipherOpenFactory cipherFactory({ + required String path, + required String key, + required SqliteOptions options, +}) { + return _NativeCipherOpenFactory(path: path, key: key, sqliteOptions: options); +} diff --git a/packages/powersync_sqlcipher/lib/src/stub.dart b/packages/powersync_sqlcipher/lib/src/stub.dart new file mode 100644 index 00000000..2e7744d6 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/stub.dart @@ -0,0 +1,10 @@ +import '../powersync.dart'; +import '../sqlite_async.dart'; + +PowerSyncSQLCipherOpenFactory cipherFactory({ + required String path, + required String key, + required SqliteOptions options, +}) { + throw UnsupportedError('Unsupported platform for powersync_sqlcipher'); +} diff --git a/packages/powersync_sqlcipher/lib/src/web_encryption.dart b/packages/powersync_sqlcipher/lib/src/web_encryption.dart new file mode 100644 index 00000000..60dcd573 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/web_encryption.dart @@ -0,0 +1,46 @@ +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/web.dart'; + +import '../powersync.dart'; + +final class _WebEncryptionFactory extends PowerSyncWebOpenFactory + implements PowerSyncSQLCipherOpenFactory { + @override + final String key; + + _WebEncryptionFactory({ + required super.path, + required this.key, + super.sqliteOptions, + }); + + @override + List pragmaStatements(SqliteOpenOptions options) { + final basePragmaStatements = super.pragmaStatements(options); + return [ + // Set the encryption key as the first statement + "PRAGMA KEY = ${quoteString(key)}", + // Include the default statements afterwards + for (var statement in basePragmaStatements) statement + ]; + } + + @override + Future connectToWorker( + WebSqlite sqlite, String name) async { + return sqlite.connectToRecommended( + name, + additionalOptions: PowerSyncAdditionalOpenOptions( + useMultipleCiphersVfs: true, + ), + ); + } +} + +PowerSyncSQLCipherOpenFactory cipherFactory({ + required String path, + required String key, + required SqliteOptions options, +}) { + return _WebEncryptionFactory(path: path, key: key, sqliteOptions: options); +} diff --git a/packages/powersync_sqlcipher/pubspec.yaml b/packages/powersync_sqlcipher/pubspec.yaml index ef80b834..e047d18d 100644 --- a/packages/powersync_sqlcipher/pubspec.yaml +++ b/packages/powersync_sqlcipher/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync_sqlcipher -version: 0.1.3 +version: 0.1.4 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Flutter SDK - sync engine for building local-first apps. @@ -12,9 +12,10 @@ dependencies: flutter: sdk: flutter - powersync_core: ^1.1.1 + powersync_core: ^1.1.2 powersync_flutter_libs: ^0.4.4 sqlcipher_flutter_libs: ^0.6.4 + sqlite3_web: ^0.3.0 dev_dependencies: flutter_test: