From 329a4d8fcb01a02fe92452d33e80b1c072a3c62f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 14 Jul 2026 15:20:41 +0200 Subject: [PATCH 1/2] Enable passing custom URL sessions --- CHANGELOG.md | 1 + .../Implementation/PowerSyncDatabaseImpl.swift | 8 ++++---- .../Implementation/sync/HttpClient.swift | 2 -- .../Implementation/sync/SyncCoordinator.swift | 9 +++++++-- Sources/PowerSync/PowerSyncDatabase.swift | 4 ++-- .../Protocol/PowerSyncDatabaseProtocol.swift | 15 +++++++++++++-- .../Kotlin/ActiveInstanceStoreTests.swift | 8 ++++---- Tests/PowerSyncTests/Kotlin/SqlCursorTests.swift | 2 +- Tests/PowerSyncTests/SyncTests.swift | 4 ++-- 9 files changed, 34 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47bd11..c630494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix `SyncStatus.asFlow()` emitting the same object, causing SwiftUI to miss updates. * Add the `ObservableSyncStatus` utility, which can be used to track Sync Status updates through an `@Observable` class. * Clear `SyncStatus.uploadError` after a successful upload. +* Allow passing a custom `URLSession` in `SyncClientConfiguration`. ## 1.14.4 diff --git a/Sources/PowerSync/Implementation/PowerSyncDatabaseImpl.swift b/Sources/PowerSync/Implementation/PowerSyncDatabaseImpl.swift index 2e68427..0b63769 100644 --- a/Sources/PowerSync/Implementation/PowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Implementation/PowerSyncDatabaseImpl.swift @@ -6,7 +6,7 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { let group: ActiveDatabaseGroup let syncStatus = SwiftSyncStatus() private let dbFilename: String? - private let httpClient: HttpClient + private let customHttpClient: HttpClient? private let initializer = DatabaseInitializationAction() let pool: any SQLiteConnectionPoolProtocol let schema: AsyncMutex @@ -17,13 +17,13 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { activeInstanceStore: DatabaseGroupCollection = .shared, logger: any LoggerProtocol, pool: any SQLiteConnectionPoolProtocol, - httpClient: HttpClient, + customHttpClient: HttpClient?, schema: Schema ) { self.dbFilename = dbFilename self.logger = logger self.schema = AsyncMutex(schema) - self.httpClient = httpClient + self.customHttpClient = customHttpClient self.pool = pool self.group = activeInstanceStore.referenceGroup(identifier: identifier, logger: logger) } @@ -133,7 +133,7 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { func connect(connector: any PowerSyncBackendConnectorProtocol, options: ConnectOptions?) async throws { try await initialize() - await group.syncCoordinator.connect(db: self, connector: connector, options: options ?? ConnectOptions(), client: httpClient) + await group.syncCoordinator.connect(db: self, connector: connector, options: options ?? ConnectOptions(), client: customHttpClient) } func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws { diff --git a/Sources/PowerSync/Implementation/sync/HttpClient.swift b/Sources/PowerSync/Implementation/sync/HttpClient.swift index 11cf252..f171d8d 100644 --- a/Sources/PowerSync/Implementation/sync/HttpClient.swift +++ b/Sources/PowerSync/Implementation/sync/HttpClient.swift @@ -79,8 +79,6 @@ struct PlatformHttpClient: HttpClient { let (data, response) = try await session.data(for: request) return (response as! HTTPURLResponse, data) } - - static let shared = PlatformHttpClient(session: .shared) } /// A wrapper around a ``HttpClient`` emitting log events for responses and sync lines. diff --git a/Sources/PowerSync/Implementation/sync/SyncCoordinator.swift b/Sources/PowerSync/Implementation/sync/SyncCoordinator.swift index e2b2bbf..17a4f8a 100644 --- a/Sources/PowerSync/Implementation/sync/SyncCoordinator.swift +++ b/Sources/PowerSync/Implementation/sync/SyncCoordinator.swift @@ -3,12 +3,17 @@ actor SyncCoordinator { nonisolated let streams = StreamTracker() private var activeSync: Task? - func connect(db: PowerSyncDatabaseImpl, connector: PowerSyncBackendConnectorProtocol, options: ConnectOptions, client: HttpClient) async { + func connect(db: PowerSyncDatabaseImpl, connector: PowerSyncBackendConnectorProtocol, options: ConnectOptions, client: HttpClient?) async { if let task = activeSync { await self.finishSyncTask(task: task) } + + func defaultHttpClient() -> HttpClient { + let session = options.clientConfiguration?.urlSession ?? .shared + return PlatformHttpClient(session: session) + } - var client = client + var client = client ?? defaultHttpClient() if let logger = options.clientConfiguration?.requestLogger { client = LoggingClient(inner: client, logger: logger) } diff --git a/Sources/PowerSync/PowerSyncDatabase.swift b/Sources/PowerSync/PowerSyncDatabase.swift index f9ed966..016f891 100644 --- a/Sources/PowerSync/PowerSyncDatabase.swift +++ b/Sources/PowerSync/PowerSyncDatabase.swift @@ -36,7 +36,7 @@ public func PowerSyncDatabase( activeInstanceStore: group, logger: logger, pool: pool, - httpClient: PlatformHttpClient.shared, + customHttpClient: nil, schema: schema ) } @@ -66,7 +66,7 @@ public func OpenedPowerSyncDatabase( identifier: identifier, logger: logger, pool: pool, - httpClient: PlatformHttpClient.shared, + customHttpClient: nil, schema: schema ) } diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index 1fdc16c..30160c1 100644 --- a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift @@ -13,10 +13,21 @@ public struct SyncClientConfiguration: Sendable { /// - SeeAlso: `SyncRequestLoggerConfiguration` for configuration options public let requestLogger: SyncRequestLoggerConfiguration? + /// The URL session to use when connecting to the PowerSync service. + /// + /// Customizing this may be convenient to add additional headers or to otherwise alter requests + /// sent by the PowerSync Swift SDK. + public var urlSession: URLSession + /// Creates a new sync client configuration. /// - Parameter requestLogger: Optional network logger configuration - public init(requestLogger: SyncRequestLoggerConfiguration? = nil) { + /// - Parameter urlSession: Optional custom URL session to use for network requests. + public init( + requestLogger: SyncRequestLoggerConfiguration? = nil, + urlSession: URLSession = .shared, + ) { self.requestLogger = requestLogger + self.urlSession = urlSession } } @@ -101,7 +112,7 @@ public struct ConnectOptions: Sendable { params: JsonParam = [:], clientConfiguration: SyncClientConfiguration? = nil, appMetadata: [String: String] = [:], - includeDefaultStreams: Bool = true + includeDefaultStreams: Bool = true, ) { self.crudThrottle = crudThrottle self.retryDelay = retryDelay diff --git a/Tests/PowerSyncTests/Kotlin/ActiveInstanceStoreTests.swift b/Tests/PowerSyncTests/Kotlin/ActiveInstanceStoreTests.swift index 30aedc3..bc089fd 100644 --- a/Tests/PowerSyncTests/Kotlin/ActiveInstanceStoreTests.swift +++ b/Tests/PowerSyncTests/Kotlin/ActiveInstanceStoreTests.swift @@ -9,10 +9,10 @@ struct MultipleInstanceTest { let logger = DefaultLogger(minSeverity: .warning, writers: [logWriter]) let schema = Schema() - let a = PowerSyncDatabaseImpl(identifier: "id", logger: logger, pool: pool, httpClient: PlatformHttpClient.shared, schema: schema) + let a = PowerSyncDatabaseImpl(identifier: "id", logger: logger, pool: pool, customHttpClient: nil, schema: schema) try #require(logWriter.getLogs().isEmpty) - let b = PowerSyncDatabaseImpl(identifier: "id", logger: logger, pool: pool, httpClient: PlatformHttpClient.shared, schema: schema) + let b = PowerSyncDatabaseImpl(identifier: "id", logger: logger, pool: pool, customHttpClient: nil, schema: schema) let _ = try #require(logWriter.getLogs().first { $0.contains("Multiple PowerSync instances for the same database have been detected.") }) // Ensure databases are kept around until the end of the test (if a gets closed before, we would't see the warning). @@ -27,10 +27,10 @@ struct MultipleInstanceTest { let schema = Schema() do { - let _ = PowerSyncDatabaseImpl(identifier: "id2", logger: logger, pool: pool, httpClient: PlatformHttpClient.shared, schema: schema) + let _ = PowerSyncDatabaseImpl(identifier: "id2", logger: logger, pool: pool, customHttpClient: nil, schema: schema) } - let b = PowerSyncDatabaseImpl(identifier: "id2", logger: logger, pool: pool, httpClient: PlatformHttpClient.shared, schema: schema) + let b = PowerSyncDatabaseImpl(identifier: "id2", logger: logger, pool: pool, customHttpClient: nil, schema: schema) try #require(logWriter.getLogs().isEmpty) let _ = consume b } diff --git a/Tests/PowerSyncTests/Kotlin/SqlCursorTests.swift b/Tests/PowerSyncTests/Kotlin/SqlCursorTests.swift index b88dd50..17acd90 100644 --- a/Tests/PowerSyncTests/Kotlin/SqlCursorTests.swift +++ b/Tests/PowerSyncTests/Kotlin/SqlCursorTests.swift @@ -70,7 +70,7 @@ final class SqlCursorTests: XCTestCase { activeInstanceStore: DatabaseGroupCollection(), logger: DefaultLogger(), pool: AsyncConnectionPool(location: .inMemory, logger: DefaultLogger()), - httpClient: PlatformHttpClient.shared, + customHttpClient: nil, schema: schema, ) try await database.disconnectAndClear() diff --git a/Tests/PowerSyncTests/SyncTests.swift b/Tests/PowerSyncTests/SyncTests.swift index 62a2142..50cd7a3 100644 --- a/Tests/PowerSyncTests/SyncTests.swift +++ b/Tests/PowerSyncTests/SyncTests.swift @@ -740,7 +740,7 @@ class InMemorySyncIntegrationTests { } @Test func subscriptionsUpdateWhileOffline() async throws { - let db = openDatabase(PlatformHttpClient.shared) + let db = openDatabase(PlatformHttpClient(session: .shared)) // Make sure the database is initialized try await db.readLock { _ in } var statusUpdates = db.currentStatus.asFlow().makeAsyncIterator() @@ -823,7 +823,7 @@ private func openDatabase(_ client: any HttpClient, schema: Schema = defaultSche activeInstanceStore: DatabaseGroupCollection(), logger: logger, pool: AsyncConnectionPool(location: .inMemory, logger: DefaultLogger()), - httpClient: client, + customHttpClient: client, schema: schema, ) } From 32f48362d7dbdcfd797182e47a36c2b4ca78ce6e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 14 Jul 2026 16:51:27 +0200 Subject: [PATCH 2/2] Add custom headers to demo --- .../PowerSyncExample/PowerSync/SystemManager.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Demos/PowerSyncExample/PowerSyncExample/PowerSync/SystemManager.swift b/Demos/PowerSyncExample/PowerSyncExample/PowerSync/SystemManager.swift index f250997..2cf5c4d 100644 --- a/Demos/PowerSyncExample/PowerSyncExample/PowerSync/SystemManager.swift +++ b/Demos/PowerSyncExample/PowerSyncExample/PowerSync/SystemManager.swift @@ -73,6 +73,12 @@ final class SystemManager { func connect() async { do { + // Passing a custom URL session is not required, but it can be used to intercept HTTP requests + // or to configure additional headers like shown here. + let config = URLSessionConfiguration.ephemeral + config.httpAdditionalHeaders = ["x-my-custom-header": "example"] + let session = URLSession(configuration: config) + try await db.connect( connector: connector, options: ConnectOptions( @@ -81,7 +87,8 @@ final class SystemManager { requestLevel: .headers ) { message in self.db.logger.debug(message, tag: "SyncRequest") - } + }, + urlSession: session ) ) )