From 45c85a1fc362630a42d597921120fe800bf0ba3b Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 25 Nov 2025 11:15:49 +0200 Subject: [PATCH 1/2] feat: add app_metadata to rust sync requests --- crates/core/src/sync/interface.rs | 17 +++++++++++++++++ crates/core/src/sync/streaming_sync.rs | 1 + dart/test/sync_test.dart | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/crates/core/src/sync/interface.rs b/crates/core/src/sync/interface.rs index f97d98bd..03bbce1f 100644 --- a/crates/core/src/sync/interface.rs +++ b/crates/core/src/sync/interface.rs @@ -12,6 +12,7 @@ use crate::sync::storage_adapter::StorageAdapter; use crate::sync::subscriptions::{StreamKey, apply_subscriptions}; use alloc::borrow::Cow; use alloc::boxed::Box; +use alloc::collections::btree_map::BTreeMap; use alloc::rc::Rc; use alloc::{string::String, vec::Vec}; use powersync_sqlite_nostd::bindings::SQLITE_RESULT_SUBTYPE; @@ -40,6 +41,19 @@ pub struct StartSyncStream { /// We will increase the expiry date for those streams at the time we connect and disconnect. #[serde(default)] pub active_streams: Rc>, + /// Application metadata to include in the request when opening a sync stream. + /// + /// This should only contain a JSON map of strings. + /// + /// We use `BTreeMap` instead of `serde_json::Map` + /// (like `parameters` uses) because: + /// 1. It enforces type safety at compile time - values must be strings, not arbitrary JSON + /// 2. It requires no runtime validation to ensure values are strings + /// 3. It serializes to the same JSON format (a map with string values) + /// 4. `serde_json::Map` doesn't implement `Serialize`/`Deserialize` - the + /// `serde_json::Map` type only supports `serde_json::Value` as the value type, not `String` + #[serde(default)] + pub app_metadata: Option>, } impl StartSyncStream { @@ -55,6 +69,7 @@ impl Default for StartSyncStream { schema: Default::default(), include_defaults: Self::include_defaults_by_default(), active_streams: Default::default(), + app_metadata: Default::default(), } } } @@ -159,6 +174,8 @@ pub struct StreamingSyncRequest { pub client_id: String, pub parameters: Option>, pub streams: Rc, + #[serde(skip_serializing_if = "Option::is_none")] + pub app_metadata: Option>, } #[derive(Debug, Serialize, PartialEq)] diff --git a/crates/core/src/sync/streaming_sync.rs b/crates/core/src/sync/streaming_sync.rs index 0a170cf0..c31c3397 100644 --- a/crates/core/src/sync/streaming_sync.rs +++ b/crates/core/src/sync/streaming_sync.rs @@ -886,6 +886,7 @@ impl StreamingSyncIteration { client_id: client_id(self.db)?, parameters: self.options.parameters.take(), streams: stream_subscriptions.request.clone(), + app_metadata: self.options.app_metadata.take(), }; event diff --git a/dart/test/sync_test.dart b/dart/test/sync_test.dart index 5adaf19a..95c6cd28 100644 --- a/dart/test/sync_test.dart +++ b/dart/test/sync_test.dart @@ -179,6 +179,28 @@ void _syncTests({ }); }); + syncTest('app_metadata is passed to EstablishSyncStream request', (_) { + final startInstructions = invokeControlRaw( + 'start', + json.encode({ + 'app_metadata': {'key1': 'value1', 'key2': 'value2'} + }), + ); + + expect( + startInstructions, + contains( + containsPair( + 'EstablishSyncStream', + containsPair( + 'request', + containsPair('app_metadata', {'key1': 'value1', 'key2': 'value2'}), + ), + ), + ), + ); + }); + test('handles connection events', () { invokeControl('start', null); expect(invokeControl('connection', 'established'), [ From caa6552a6dd3a650af2e3c82c9202781f6a345a3 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 26 Nov 2025 15:18:14 +0100 Subject: [PATCH 2/2] Use raw json value --- crates/core/src/sync/interface.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/core/src/sync/interface.rs b/crates/core/src/sync/interface.rs index 03bbce1f..ac2883c5 100644 --- a/crates/core/src/sync/interface.rs +++ b/crates/core/src/sync/interface.rs @@ -12,13 +12,13 @@ use crate::sync::storage_adapter::StorageAdapter; use crate::sync::subscriptions::{StreamKey, apply_subscriptions}; use alloc::borrow::Cow; use alloc::boxed::Box; -use alloc::collections::btree_map::BTreeMap; use alloc::rc::Rc; use alloc::{string::String, vec::Vec}; use powersync_sqlite_nostd::bindings::SQLITE_RESULT_SUBTYPE; use powersync_sqlite_nostd::{self as sqlite, ColumnType}; use powersync_sqlite_nostd::{Connection, Context}; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; use sqlite::{ResultCode, Value}; use crate::sync::BucketPriority; @@ -41,19 +41,8 @@ pub struct StartSyncStream { /// We will increase the expiry date for those streams at the time we connect and disconnect. #[serde(default)] pub active_streams: Rc>, - /// Application metadata to include in the request when opening a sync stream. - /// - /// This should only contain a JSON map of strings. - /// - /// We use `BTreeMap` instead of `serde_json::Map` - /// (like `parameters` uses) because: - /// 1. It enforces type safety at compile time - values must be strings, not arbitrary JSON - /// 2. It requires no runtime validation to ensure values are strings - /// 3. It serializes to the same JSON format (a map with string values) - /// 4. `serde_json::Map` doesn't implement `Serialize`/`Deserialize` - the - /// `serde_json::Map` type only supports `serde_json::Value` as the value type, not `String` #[serde(default)] - pub app_metadata: Option>, + pub app_metadata: Option>, } impl StartSyncStream { @@ -175,7 +164,7 @@ pub struct StreamingSyncRequest { pub parameters: Option>, pub streams: Rc, #[serde(skip_serializing_if = "Option::is_none")] - pub app_metadata: Option>, + pub app_metadata: Option>, } #[derive(Debug, Serialize, PartialEq)]