diff --git a/demos/supabase-todolist/lib/powersync.dart b/demos/supabase-todolist/lib/powersync.dart index 412e53c7..d6e5973d 100644 --- a/demos/supabase-todolist/lib/powersync.dart +++ b/demos/supabase-todolist/lib/powersync.dart @@ -153,7 +153,9 @@ Future getDatabasePath() async { return join(dir.path, dbFilename); } -const options = SyncOptions(syncImplementation: SyncClientImplementation.rust); +const options = SyncOptions( + syncImplementation: SyncClientImplementation.rust, + appMetadata: {'app_version': '1.0.1'}); Future openDatabase() async { // Open the local database diff --git a/packages/powersync_core/lib/src/sync/options.dart b/packages/powersync_core/lib/src/sync/options.dart index 12e4c331..4d228ca4 100644 --- a/packages/powersync_core/lib/src/sync/options.dart +++ b/packages/powersync_core/lib/src/sync/options.dart @@ -3,6 +3,11 @@ import 'package:meta/meta.dart'; /// Options that affect how the sync client connects to the sync service. final class SyncOptions { + /// A map of application metadata that is passed to the PowerSync service. + /// + /// Application metadata that will be displayed in PowerSync service logs. + final Map? appMetadata; + /// A JSON object that is passed to the sync service and forwarded to sync /// rules. /// @@ -39,12 +44,14 @@ final class SyncOptions { this.params, this.syncImplementation = SyncClientImplementation.defaultClient, this.includeDefaultStreams, + this.appMetadata, }); SyncOptions _copyWith({ Duration? crudThrottleTime, Duration? retryDelay, Map? params, + Map? appMetadata, }) { return SyncOptions( crudThrottleTime: crudThrottleTime ?? this.crudThrottleTime, @@ -52,6 +59,7 @@ final class SyncOptions { params: params ?? this.params, syncImplementation: syncImplementation, includeDefaultStreams: includeDefaultStreams, + appMetadata: appMetadata ?? this.appMetadata, ); } } @@ -88,14 +96,18 @@ extension type ResolvedSyncOptions(SyncOptions source) { Duration? crudThrottleTime, Duration? retryDelay, Map? params, + Map? appMetadata, }) { return ResolvedSyncOptions((source ?? SyncOptions())._copyWith( crudThrottleTime: crudThrottleTime, retryDelay: retryDelay, params: params, + appMetadata: appMetadata, )); } + Map get appMetadata => source.appMetadata ?? const {}; + Duration get crudThrottleTime => source.crudThrottleTime ?? const Duration(milliseconds: 10); diff --git a/packages/powersync_core/lib/src/sync/protocol.dart b/packages/powersync_core/lib/src/sync/protocol.dart index 4e07334b..8ca27312 100644 --- a/packages/powersync_core/lib/src/sync/protocol.dart +++ b/packages/powersync_core/lib/src/sync/protocol.dart @@ -247,15 +247,18 @@ class StreamingSyncRequest { bool includeChecksum = true; String clientId; Map? parameters; + Map? appMetadata; - StreamingSyncRequest(this.buckets, this.parameters, this.clientId); + StreamingSyncRequest( + this.buckets, this.parameters, this.clientId, this.appMetadata); Map toJson() { final Map json = { 'buckets': buckets, 'include_checksum': includeChecksum, 'raw_data': true, - 'client_id': clientId + 'client_id': clientId, + 'app_metadata': appMetadata, }; if (parameters != null) { diff --git a/packages/powersync_core/lib/src/sync/streaming_sync.dart b/packages/powersync_core/lib/src/sync/streaming_sync.dart index 0d7ec7a7..3488d13f 100644 --- a/packages/powersync_core/lib/src/sync/streaming_sync.dart +++ b/packages/powersync_core/lib/src/sync/streaming_sync.dart @@ -12,14 +12,14 @@ import 'package:powersync_core/src/sync/options.dart'; import 'package:powersync_core/src/user_agent/user_agent.dart'; import 'package:sqlite_async/mutex.dart'; -import 'bucket_storage.dart'; import '../crud.dart'; +import 'bucket_storage.dart'; import 'instruction.dart'; import 'internal_connector.dart'; import 'mutable_sync_status.dart'; +import 'protocol.dart'; import 'stream_utils.dart'; import 'sync_status.dart'; -import 'protocol.dart'; typedef SubscribedStream = ({String name, String parameters}); @@ -339,8 +339,8 @@ class StreamingSyncImplementation implements StreamingSync { Checkpoint? targetCheckpoint; - var requestStream = _streamingSyncRequest( - StreamingSyncRequest(bucketRequests, options.params, clientId!)) + var requestStream = _streamingSyncRequest(StreamingSyncRequest( + bucketRequests, options.params, clientId!, options.appMetadata)) .map(ReceivedLine.new); var merged = addBroadcast(requestStream, _nonLineSyncEvents.stream); @@ -633,6 +633,7 @@ final class _ActiveRustStreamingIteration { await _control( 'start', convert.json.encode({ + 'app_metadata': sync.options.appMetadata, 'parameters': sync.options.params, 'schema': convert.json.decode(sync.schemaJson), 'include_defaults': sync.options.includeDefaultStreams, diff --git a/packages/powersync_core/test/sync/stream_test.dart b/packages/powersync_core/test/sync/stream_test.dart index 1625656c..afc81b45 100644 --- a/packages/powersync_core/test/sync/stream_test.dart +++ b/packages/powersync_core/test/sync/stream_test.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:async/async.dart'; import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; - import 'package:test/test.dart'; import '../server/sync_server/in_memory_sync_server.dart'; @@ -260,4 +259,41 @@ void main() { ); a.unsubscribe(); }); + + test('passes app metadata to the server (rust)', () async { + options = SyncOptions( + syncImplementation: SyncClientImplementation.rust, + appMetadata: {'foo': 'bar'}, + ); + + await waitForConnection(); + + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'app_metadata', + containsPair('foo', 'bar'), + ), + ); + }); + + test('passes app metadata to the server (legacy sync client)', () async { + options = SyncOptions( + // ignore: deprecated_member_use_from_same_package + syncImplementation: SyncClientImplementation.dart, + appMetadata: {'foo': 'bar'}, + ); + + await waitForConnection(); + + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'app_metadata', + containsPair('foo', 'bar'), + ), + ); + }); }