Skip to content
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## 1.4.0

* Added the ability to log PowerSync sync network requests.

```swift
struct InlineLogger: SyncRequestLogger {
func log(_ message: String) {
print("Network: \(message)")
}
}

try await db.connect(
connector: connector,
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
logLevel: .headers,
logger: InlineLogger()
)
)
)
)
```

## 1.3.0

* Use version `0.4.2` of the PowerSync core extension, which improves the reliability
Expand Down
18 changes: 17 additions & 1 deletion Demo/PowerSyncExample/PowerSync/SystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ func getAttachmentsDirectoryPath() throws -> String {

let logTag = "SystemManager"

struct InlineLogger: SyncRequestLogger {
func log(_ message: String) {
print("Network: \(message)")
}
}

@Observable
class SystemManager {
let connector = SupabaseConnector()
Expand Down Expand Up @@ -70,7 +76,17 @@ class SystemManager {

func connect() async {
do {
try await db.connect(connector: connector)
try await db.connect(
connector: connector,
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
logLevel: .headers,
logger: InlineLogger()
)
)
)
)
try await attachments?.startSync()
} catch {
print("Unexpected error: \(error.localizedDescription)") // Catches any other error
Expand Down
29 changes: 29 additions & 0 deletions Sources/PowerSync/Kotlin/KotlinNetworkLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PowerSyncKotlin

extension SyncRequestLogLevel {
func toKotlin() -> SwiftSyncRequestLogLevel {
switch self {
case .all:
return SwiftSyncRequestLogLevel.all
case .headers:
return SwiftSyncRequestLogLevel.headers
case .body:
return SwiftSyncRequestLogLevel.body
case .info:
return SwiftSyncRequestLogLevel.info
case .none:
return SwiftSyncRequestLogLevel.none
}
}
}

extension SyncRequestLoggerConfiguration {
func toKotlinConfig() -> SwiftRequestLoggerConfig {
return SwiftRequestLoggerConfig(
logLevel: self.logLevel.toKotlin(),
log: { [logger] message in
logger.log(message)
}
)
}
}
8 changes: 2 additions & 6 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
)

let resolvedOptions = options ?? ConnectOptions()
let useWebsockets = switch (resolvedOptions.connectionMethod) {
case .http: false
case .webSocket: true
}

try await kotlinDatabase.connect(
connector: connectorAdapter,
Expand All @@ -64,8 +60,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
params: resolvedOptions.params.mapValues { $0.toKotlinMap() },
options: createSyncOptions(
newClient: resolvedOptions.newClientImplementation,
webSocket: useWebsockets,
userAgent: "PowerSync Swift SDK"
userAgent: "PowerSync Swift SDK",
loggingConfig: resolvedOptions.clientConfiguration?.requestLogger?.toKotlinConfig()
)
)
}
Expand Down
60 changes: 41 additions & 19 deletions Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import Foundation

/// Configuration for the sync client used to connect to the PowerSync service.
///
/// Provides options to customize network behavior and logging for PowerSync
/// HTTP requests and responses.
public struct SyncClientConfiguration {
/// Optional configuration for logging PowerSync HTTP requests.
///
/// When provided, network requests will be logged according to the
/// specified `SyncRequestLogLevel`. The `logLevel` is set during initialization
/// and remains constant throughout the PowerSync session.
///
/// Set to `nil` to disable request logging entirely.
///
/// - SeeAlso: `SyncRequestLoggerConfiguration` for configuration options
public let requestLogger: SyncRequestLoggerConfiguration?

/// Creates a new sync client configuration.
/// - Parameter requestLogger: Optional network logger configuration
public init(requestLogger: SyncRequestLoggerConfiguration? = nil) {
self.requestLogger = requestLogger
}
}

/// Options for configuring a PowerSync connection.
///
/// Provides optional parameters to customize sync behavior such as throttling and retry policies.
Expand Down Expand Up @@ -41,31 +64,37 @@ public struct ConnectOptions {
/// We encourage interested users to try the new client.
@_spi(PowerSyncExperimental)
public var newClientImplementation: Bool

/// The connection method used to connect to the Powersync service.
/// Configuration for the sync client used for PowerSync requests.
///
/// The default method is ``ConnectionMethod/http``. Using ``ConnectionMethod/webSocket(_:)`` can
/// improve performance as a more efficient binary protocol is used. However, using the websocket connection method
/// requires enabling ``ConnectOptions/newClientImplementation``.
@_spi(PowerSyncExperimental)
public var connectionMethod: ConnectionMethod
/// Provides options to customize network behavior including logging of HTTP
/// requests and responses. When `nil`, default HTTP client settings are used
/// with no network logging.
///
/// Set this to configure network logging or other HTTP client behaviors
/// specific to PowerSync operations.
///
/// - SeeAlso: `SyncClientConfiguration` for available configuration options
public var clientConfiguration: SyncClientConfiguration?

/// Initializes a `ConnectOptions` instance with optional values.
///
/// - Parameters:
/// - crudThrottle: TimeInterval between CRUD operations in milliseconds. Defaults to `1` second.
/// - retryDelay: Delay TimeInterval between retry attempts in milliseconds. Defaults to `5` seconds.
/// - params: Custom sync parameters to send to the server. Defaults to an empty dictionary.
/// - clientConfiguration: Configuration for the HTTP client used to connect to PowerSync.
public init(
crudThrottle: TimeInterval = 1,
retryDelay: TimeInterval = 5,
params: JsonParam = [:]
params: JsonParam = [:],
clientConfiguration: SyncClientConfiguration? = nil
) {
self.crudThrottle = crudThrottle
self.retryDelay = retryDelay
self.params = params
self.newClientImplementation = false
self.connectionMethod = .http
self.clientConfiguration = clientConfiguration
}

/// Initializes a ``ConnectOptions`` instance with optional values, including experimental options.
Expand All @@ -75,22 +104,16 @@ public struct ConnectOptions {
retryDelay: TimeInterval = 5,
params: JsonParam = [:],
newClientImplementation: Bool = false,
connectionMethod: ConnectionMethod = .http
clientConfiguration: SyncClientConfiguration? = nil
) {
self.crudThrottle = crudThrottle
self.retryDelay = retryDelay
self.params = params
self.newClientImplementation = newClientImplementation
self.connectionMethod = connectionMethod
self.clientConfiguration = clientConfiguration
}
}

@_spi(PowerSyncExperimental)
public enum ConnectionMethod {
case http
case webSocket
}

/// A PowerSync managed database.
///
/// Use one instance per database file.
Expand All @@ -108,7 +131,6 @@ public protocol PowerSyncDatabaseProtocol: Queries {
/// Wait for the first sync to occur
func waitForFirstSync() async throws


/// Replace the schema with a new version. This is for advanced use cases - typically the schema
/// should just be specified once in the constructor.
///
Expand Down Expand Up @@ -247,7 +269,7 @@ public extension PowerSyncDatabaseProtocol {
}

func disconnectAndClear(clearLocal: Bool = true) async throws {
try await self.disconnectAndClear(clearLocal: clearLocal)
try await disconnectAndClear(clearLocal: clearLocal)
}

func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? {
Expand Down
54 changes: 54 additions & 0 deletions Sources/PowerSync/Protocol/SyncRequestLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// A logger which handles PowerSync network request logs.
///
/// Implement this protocol to receive network request logging messages at the level
/// specified in `SyncRequestLoggerConfiguration`. The `log(_:)` method will be called
/// for each network event that meets the configured logging criteria.
public protocol SyncRequestLogger {
/// Logs a network-related message.
/// - Parameter message: The formatted log message to record
func log(_ message: String)
}

/// Level of logs to expose to a `SyncRequestLogger` handler.
///
/// Controls the verbosity of network logging for PowerSync HTTP requests.
/// The log level is configured once during initialization and determines
/// which network events will be logged throughout the session.
public enum SyncRequestLogLevel {
/// Log all network activity including headers, body, and info
case all
/// Log only request/response headers
case headers
/// Log only request/response body content
case body
/// Log basic informational messages about requests
case info
/// Disable all network logging
case none
}

/// Configuration for PowerSync HTTP request logging.
///
/// This configuration is set once during initialization and used throughout
/// the PowerSync session. The `logLevel` determines which network events
/// are logged, while the `logger` handles the actual log output.
///
/// - Note: The log level cannot be changed after initialization. A new call to `PowerSyncDatabase.connect` is required to change the level.
public struct SyncRequestLoggerConfiguration {
/// The logging level that determines which network events are logged.
/// Set once during initialization and used throughout the session.
public let logLevel: SyncRequestLogLevel

/// The logger instance that receives network request log messages.
/// Must conform to `SyncRequestLogger` protocol.
public let logger: SyncRequestLogger

/// Creates a new network logger configuration.
/// - Parameters:
/// - logLevel: The `SyncRequestLogLevel` to use for filtering log messages
/// - logger: A `SyncRequestLogger` instance to handle log output
public init(logLevel: SyncRequestLogLevel, logger: SyncRequestLogger) {
self.logLevel = logLevel
self.logger = logger
}
}
53 changes: 53 additions & 0 deletions Tests/PowerSyncTests/ConnectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,57 @@ final class ConnectTests: XCTestCase {
await fulfillment(of: [expectation], timeout: 5)
watchTask.cancel()
}

func testSyncHTTPLogs() async throws {
let expectation = XCTestExpectation(
description: "Should log a request to the PowerSync endpoint"
)

let fakeUrl = "https://fakepowersyncinstance.fakepowersync.local"

struct InlineLogger: SyncRequestLogger {
let logger: (_: String) -> Void

func log(_ message: String) {
logger(message)
}
}

let testLogger = InlineLogger { message in
// We want to see a request to the specified instance
if message.contains(fakeUrl) {
expectation.fulfill()
}
}

class TestConnector: PowerSyncBackendConnector {
let url: String

init(url: String) {
self.url = url
}

override func fetchCredentials() async throws -> PowerSyncCredentials? {
PowerSyncCredentials(
endpoint: url,
token: "123"
)
}
}

try await database.connect(
connector: TestConnector(url: fakeUrl),
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
logLevel:
.all,
logger: testLogger
)
)
)
)

await fulfillment(of: [expectation], timeout: 5)
}
}
Loading