Skip to content

Commit 8188bb9

Browse files
feat: Custom App Metadata (#354)
1 parent 2cf72ab commit 8188bb9

File tree

7 files changed

+76
-9
lines changed

7 files changed

+76
-9
lines changed

demos/supabase-todolist/lib/powersync.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ Future<String> getDatabasePath() async {
153153
return join(dir.path, dbFilename);
154154
}
155155

156-
const options = SyncOptions(syncImplementation: SyncClientImplementation.rust);
156+
const options = SyncOptions(
157+
syncImplementation: SyncClientImplementation.rust,
158+
appMetadata: {'app_version': '1.0.1'});
157159

158160
Future<void> openDatabase() async {
159161
// Open the local database

packages/powersync_core/lib/src/sync/options.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import 'package:meta/meta.dart';
33

44
/// Options that affect how the sync client connects to the sync service.
55
final class SyncOptions {
6+
/// A map of application metadata that is passed to the PowerSync service.
7+
///
8+
/// Application metadata that will be displayed in PowerSync service logs.
9+
final Map<String, String>? appMetadata;
10+
611
/// A JSON object that is passed to the sync service and forwarded to sync
712
/// rules.
813
///
@@ -39,19 +44,22 @@ final class SyncOptions {
3944
this.params,
4045
this.syncImplementation = SyncClientImplementation.defaultClient,
4146
this.includeDefaultStreams,
47+
this.appMetadata,
4248
});
4349

4450
SyncOptions _copyWith({
4551
Duration? crudThrottleTime,
4652
Duration? retryDelay,
4753
Map<String, dynamic>? params,
54+
Map<String, String>? appMetadata,
4855
}) {
4956
return SyncOptions(
5057
crudThrottleTime: crudThrottleTime ?? this.crudThrottleTime,
5158
retryDelay: retryDelay,
5259
params: params ?? this.params,
5360
syncImplementation: syncImplementation,
5461
includeDefaultStreams: includeDefaultStreams,
62+
appMetadata: appMetadata ?? this.appMetadata,
5563
);
5664
}
5765
}
@@ -88,14 +96,18 @@ extension type ResolvedSyncOptions(SyncOptions source) {
8896
Duration? crudThrottleTime,
8997
Duration? retryDelay,
9098
Map<String, dynamic>? params,
99+
Map<String, String>? appMetadata,
91100
}) {
92101
return ResolvedSyncOptions((source ?? SyncOptions())._copyWith(
93102
crudThrottleTime: crudThrottleTime,
94103
retryDelay: retryDelay,
95104
params: params,
105+
appMetadata: appMetadata,
96106
));
97107
}
98108

109+
Map<String, String> get appMetadata => source.appMetadata ?? const {};
110+
99111
Duration get crudThrottleTime =>
100112
source.crudThrottleTime ?? const Duration(milliseconds: 10);
101113

@@ -113,13 +125,15 @@ extension type ResolvedSyncOptions(SyncOptions source) {
113125
syncImplementation: other.syncImplementation,
114126
includeDefaultStreams:
115127
other.includeDefaultStreams ?? includeDefaultStreams,
128+
appMetadata: other.appMetadata ?? appMetadata,
116129
);
117130

118131
final didChange = !_mapEquality.equals(newOptions.params, params) ||
119132
newOptions.crudThrottleTime != crudThrottleTime ||
120133
newOptions.retryDelay != retryDelay ||
121134
newOptions.syncImplementation != source.syncImplementation ||
122-
newOptions.includeDefaultStreams != includeDefaultStreams;
135+
newOptions.includeDefaultStreams != includeDefaultStreams ||
136+
!_mapEquality.equals(newOptions.appMetadata, appMetadata);
123137
return (ResolvedSyncOptions(newOptions), didChange);
124138
}
125139

packages/powersync_core/lib/src/sync/protocol.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,18 @@ class StreamingSyncRequest {
247247
bool includeChecksum = true;
248248
String clientId;
249249
Map<String, dynamic>? parameters;
250+
Map<String, String>? appMetadata;
250251

251-
StreamingSyncRequest(this.buckets, this.parameters, this.clientId);
252+
StreamingSyncRequest(
253+
this.buckets, this.parameters, this.clientId, this.appMetadata);
252254

253255
Map<String, dynamic> toJson() {
254256
final Map<String, dynamic> json = {
255257
'buckets': buckets,
256258
'include_checksum': includeChecksum,
257259
'raw_data': true,
258-
'client_id': clientId
260+
'client_id': clientId,
261+
'app_metadata': appMetadata,
259262
};
260263

261264
if (parameters != null) {

packages/powersync_core/lib/src/sync/streaming_sync.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import 'package:powersync_core/src/sync/options.dart';
1212
import 'package:powersync_core/src/user_agent/user_agent.dart';
1313
import 'package:sqlite_async/mutex.dart';
1414

15-
import 'bucket_storage.dart';
1615
import '../crud.dart';
16+
import 'bucket_storage.dart';
1717
import 'instruction.dart';
1818
import 'internal_connector.dart';
1919
import 'mutable_sync_status.dart';
20+
import 'protocol.dart';
2021
import 'stream_utils.dart';
2122
import 'sync_status.dart';
22-
import 'protocol.dart';
2323

2424
typedef SubscribedStream = ({String name, String parameters});
2525

@@ -339,8 +339,8 @@ class StreamingSyncImplementation implements StreamingSync {
339339

340340
Checkpoint? targetCheckpoint;
341341

342-
var requestStream = _streamingSyncRequest(
343-
StreamingSyncRequest(bucketRequests, options.params, clientId!))
342+
var requestStream = _streamingSyncRequest(StreamingSyncRequest(
343+
bucketRequests, options.params, clientId!, options.appMetadata))
344344
.map(ReceivedLine.new);
345345

346346
var merged = addBroadcast(requestStream, _nonLineSyncEvents.stream);
@@ -633,6 +633,7 @@ final class _ActiveRustStreamingIteration {
633633
await _control(
634634
'start',
635635
convert.json.encode({
636+
'app_metadata': sync.options.appMetadata,
636637
'parameters': sync.options.params,
637638
'schema': convert.json.decode(sync.schemaJson),
638639
'include_defaults': sync.options.includeDefaultStreams,

packages/powersync_core/lib/src/web/sync_worker.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ class _ConnectedClient {
9494
null => SyncClientImplementation.defaultClient,
9595
final name => SyncClientImplementation.values.byName(name),
9696
},
97+
appMetadata: switch (request.appMetadataEncoded) {
98+
null => null,
99+
final encodedAppMetadata => Map<String, String>.from(
100+
jsonDecode(encodedAppMetadata) as Map<String, dynamic>),
101+
},
97102
);
98103

99104
_runner = _worker.referenceSyncTask(

packages/powersync_core/lib/src/web/sync_worker_protocol.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ extension type StartSynchronization._(JSObject _) implements JSObject {
8080
required String schemaJson,
8181
String? syncParamsEncoded,
8282
UpdateSubscriptions? subscriptions,
83+
String? appMetadataEncoded,
8384
});
8485

8586
external String get databaseName;
@@ -90,6 +91,7 @@ extension type StartSynchronization._(JSObject _) implements JSObject {
9091
external String get schemaJson;
9192
external String? get syncParamsEncoded;
9293
external UpdateSubscriptions? get subscriptions;
94+
external String? get appMetadataEncoded;
9395
}
9496

9597
@anonymous
@@ -483,6 +485,10 @@ final class WorkerCommunicationChannel {
483485
final params => jsonEncode(params),
484486
},
485487
subscriptions: UpdateSubscriptions(-1, streams),
488+
appMetadataEncoded: switch (options.source.appMetadata) {
489+
null => null,
490+
final appMetadata => jsonEncode(appMetadata),
491+
},
486492
),
487493
));
488494
await completion;

packages/powersync_core/test/sync/stream_test.dart

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import 'dart:convert';
44
import 'package:async/async.dart';
55
import 'package:logging/logging.dart';
66
import 'package:powersync_core/powersync_core.dart';
7-
87
import 'package:test/test.dart';
98

109
import '../server/sync_server/in_memory_sync_server.dart';
@@ -260,4 +259,41 @@ void main() {
260259
);
261260
a.unsubscribe();
262261
});
262+
263+
test('passes app metadata to the server (rust)', () async {
264+
options = SyncOptions(
265+
syncImplementation: SyncClientImplementation.rust,
266+
appMetadata: {'foo': 'bar'},
267+
);
268+
269+
await waitForConnection();
270+
271+
final request = await syncService.waitForListener;
272+
expect(
273+
json.decode(await request.readAsString()),
274+
containsPair(
275+
'app_metadata',
276+
containsPair('foo', 'bar'),
277+
),
278+
);
279+
});
280+
281+
test('passes app metadata to the server (legacy sync client)', () async {
282+
options = SyncOptions(
283+
// ignore: deprecated_member_use_from_same_package
284+
syncImplementation: SyncClientImplementation.dart,
285+
appMetadata: {'foo': 'bar'},
286+
);
287+
288+
await waitForConnection();
289+
290+
final request = await syncService.waitForListener;
291+
expect(
292+
json.decode(await request.readAsString()),
293+
containsPair(
294+
'app_metadata',
295+
containsPair('foo', 'bar'),
296+
),
297+
);
298+
});
263299
}

0 commit comments

Comments
 (0)