diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcb0de..777a2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.5.1 (unreleased) * Update core extension to 0.4.5 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.5)) +* Additional Swift 6 Strict Concurrency Checking declarations added for remaining protocols. ## 1.5.0 diff --git a/Demo/PowerSyncExample.xcodeproj/project.pbxproj b/Demo/PowerSyncExample.xcodeproj/project.pbxproj index cbe2645..64891f7 100644 --- a/Demo/PowerSyncExample.xcodeproj/project.pbxproj +++ b/Demo/PowerSyncExample.xcodeproj/project.pbxproj @@ -645,6 +645,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -701,6 +702,7 @@ OTHER_LDFLAGS = "-lsqlite3"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; }; name = Release; @@ -742,7 +744,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "PowerSyncExample/PowerSyncExample-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -781,7 +783,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "PowerSyncExample/PowerSyncExample-Bridging-Header.h"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Demo/PowerSyncExample/Components/AddListView.swift b/Demo/PowerSyncExample/Components/AddListView.swift index b95f810..d87d411 100644 --- a/Demo/PowerSyncExample/Components/AddListView.swift +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -13,9 +13,9 @@ struct AddListView: View { Task { do { try await system.insertList(newList) - await completion(.success(true)) + completion(.success(true)) } catch { - await completion(.failure(error)) + completion(.failure(error)) throw error } } diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift index 0f37a0b..9622d8c 100644 --- a/Demo/PowerSyncExample/Components/AddTodoListView.swift +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -15,9 +15,9 @@ struct AddTodoListView: View { Task{ do { try await system.insertTodo(newTodo, listId) - await completion(.success(true)) + completion(.success(true)) } catch { - await completion(.failure(error)) + completion(.failure(error)) throw error } } diff --git a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift index 4bad48e..11ab22c 100644 --- a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift +++ b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift @@ -38,13 +38,14 @@ private enum PostgresFatalCodes { } @Observable +@MainActor // _session is mutable, limiting to the MainActor satisfies Sendable constraints final class SupabaseConnector: PowerSyncBackendConnectorProtocol { let powerSyncEndpoint: String = Secrets.powerSyncEndpoint let client: SupabaseClient = .init( supabaseURL: Secrets.supabaseURL, supabaseKey: Secrets.supabaseAnonKey, ) - var session: Session? + private(set) var session: Session? private var errorCode: String? @ObservationIgnored diff --git a/Demo/PowerSyncExample/PowerSync/SupabaseRemoteStorage.swift b/Demo/PowerSyncExample/PowerSync/SupabaseRemoteStorage.swift index a79ca95..ab82c69 100644 --- a/Demo/PowerSyncExample/PowerSync/SupabaseRemoteStorage.swift +++ b/Demo/PowerSyncExample/PowerSync/SupabaseRemoteStorage.swift @@ -2,7 +2,7 @@ import Foundation import PowerSync import Supabase -class SupabaseRemoteStorage: RemoteStorageAdapter { +final class SupabaseRemoteStorage: RemoteStorageAdapter { let storage: Supabase.StorageFileApi init(storage: Supabase.StorageFileApi) { diff --git a/Demo/PowerSyncExample/PowerSync/SystemManager.swift b/Demo/PowerSyncExample/PowerSync/SystemManager.swift index 89b3641..124a11b 100644 --- a/Demo/PowerSyncExample/PowerSync/SystemManager.swift +++ b/Demo/PowerSyncExample/PowerSync/SystemManager.swift @@ -13,13 +13,16 @@ func getAttachmentsDirectoryPath() throws -> String { let logTag = "SystemManager" +/// We use the MainActor SupabaseConnector synchronously here, this requires specifying that SystemManager runs on the MainActor +/// We don't actually block the MainActor with anything @Observable -class SystemManager { +@MainActor +final class SystemManager { let connector = SupabaseConnector() let schema = AppSchema let db: PowerSyncDatabaseProtocol - var attachments: AttachmentQueue? + let attachments: AttachmentQueue? init() { db = PowerSyncDatabase( @@ -226,25 +229,18 @@ class SystemManager { try await attachments.deleteFile( attachmentId: photoId ) { transaction, _ in - try self.deleteTodoInTX( - id: todo.id, - tx: transaction + try transaction.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [todo.id] ) } } else { - try await db.writeTransaction { transaction in - try self.deleteTodoInTX( - id: todo.id, - tx: transaction + _ = try await db.writeTransaction { transaction in + try transaction.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [todo.id] ) } } } - - private func deleteTodoInTX(id: String, tx: ConnectionContext) throws { - _ = try tx.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", - parameters: [id] - ) - } } diff --git a/Sources/PowerSync/Kotlin/KotlinTypes.swift b/Sources/PowerSync/Kotlin/KotlinTypes.swift index 8d0acc8..f85fce1 100644 --- a/Sources/PowerSync/Kotlin/KotlinTypes.swift +++ b/Sources/PowerSync/Kotlin/KotlinTypes.swift @@ -8,3 +8,7 @@ extension KotlinPowerSyncBackendConnector: @retroactive @unchecked Sendable {} extension KotlinPowerSyncCredentials: @retroactive @unchecked Sendable {} extension PowerSyncKotlin.KermitLogger: @retroactive @unchecked Sendable {} extension PowerSyncKotlin.SyncStatus: @retroactive @unchecked Sendable {} + +extension PowerSyncKotlin.CrudEntry: @retroactive @unchecked Sendable {} +extension PowerSyncKotlin.CrudBatch: @retroactive @unchecked Sendable {} +extension PowerSyncKotlin.CrudTransaction: @retroactive @unchecked Sendable {} diff --git a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift index df64951..a7fcf47 100644 --- a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift +++ b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift @@ -103,11 +103,17 @@ extension KotlinProgressWithOperationsProtocol { } } -struct KotlinProgressWithOperations: KotlinProgressWithOperationsProtocol { +struct KotlinProgressWithOperations: KotlinProgressWithOperationsProtocol, + // We can't mark PowerSyncKotlin.ProgressWithOperations as Sendable + @unchecked Sendable +{ let base: PowerSyncKotlin.ProgressWithOperations } -struct KotlinSyncDownloadProgress: KotlinProgressWithOperationsProtocol, SyncDownloadProgress { +struct KotlinSyncDownloadProgress: KotlinProgressWithOperationsProtocol, SyncDownloadProgress, + // We can't mark PowerSyncKotlin.SyncDownloadProgress as Sendable + @unchecked Sendable +{ let progress: PowerSyncKotlin.SyncDownloadProgress var base: any PowerSyncKotlin.ProgressWithOperations { diff --git a/Sources/PowerSync/Logger.swift b/Sources/PowerSync/Logger.swift index cd1c06c..d483982 100644 --- a/Sources/PowerSync/Logger.swift +++ b/Sources/PowerSync/Logger.swift @@ -3,15 +3,10 @@ import OSLog /// A log writer which prints to the standard output /// /// This writer uses `os.Logger` on iOS/macOS/tvOS/watchOS 14+ and falls back to `print` for earlier versions. -public class PrintLogWriter: LogWriterProtocol { +public final class PrintLogWriter: LogWriterProtocol { private let subsystem: String private let category: String - private lazy var logger: Any? = { - if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { - return Logger(subsystem: subsystem, category: category) - } - return nil - }() + private let logger: Sendable? /// Creates a new PrintLogWriter /// - Parameters: @@ -22,6 +17,12 @@ public class PrintLogWriter: LogWriterProtocol { { self.subsystem = subsystem self.category = category + + if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { + logger = Logger(subsystem: subsystem, category: category) + } else { + logger = nil + } } /// Logs a message with a given severity and optional tag. diff --git a/Sources/PowerSync/Protocol/LoggerProtocol.swift b/Sources/PowerSync/Protocol/LoggerProtocol.swift index 2169f86..f8e35fb 100644 --- a/Sources/PowerSync/Protocol/LoggerProtocol.swift +++ b/Sources/PowerSync/Protocol/LoggerProtocol.swift @@ -34,7 +34,7 @@ public enum LogSeverity: Int, CaseIterable, Sendable { /// A protocol for writing log messages to a specific backend or output. /// /// Conformers handle the actual writing or forwarding of log messages. -public protocol LogWriterProtocol { +public protocol LogWriterProtocol: Sendable { /// Logs a message with the given severity and optional tag. /// /// - Parameters: diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index 8493504..500dc10 100644 --- a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift @@ -4,7 +4,7 @@ import Foundation /// /// Provides options to customize network behavior and logging for PowerSync /// HTTP requests and responses. -public struct SyncClientConfiguration { +public struct SyncClientConfiguration: Sendable { /// Optional configuration for logging PowerSync HTTP requests. /// /// When provided, network requests will be logged according to the @@ -23,7 +23,7 @@ public struct SyncClientConfiguration { /// Options for configuring a PowerSync connection. /// /// Provides optional parameters to customize sync behavior such as throttling and retry policies. -public struct ConnectOptions { +public struct ConnectOptions: Sendable { /// Defaults to 1 second public static let DefaultCrudThrottle: TimeInterval = 1 diff --git a/Sources/PowerSync/Protocol/Schema/Column.swift b/Sources/PowerSync/Protocol/Schema/Column.swift index 1489666..f1d1bd0 100644 --- a/Sources/PowerSync/Protocol/Schema/Column.swift +++ b/Sources/PowerSync/Protocol/Schema/Column.swift @@ -1,7 +1,7 @@ import Foundation import PowerSyncKotlin -public protocol ColumnProtocol: Equatable { +public protocol ColumnProtocol: Equatable, Sendable { /// Name of the column. var name: String { get } /// Type of the column. @@ -15,7 +15,7 @@ public protocol ColumnProtocol: Equatable { var type: ColumnData { get } } -public enum ColumnData { +public enum ColumnData: Sendable { case text case integer case real @@ -25,7 +25,7 @@ public enum ColumnData { public struct Column: ColumnProtocol { public let name: String public let type: ColumnData - + public init( name: String, type: ColumnData @@ -33,15 +33,15 @@ public struct Column: ColumnProtocol { self.name = name self.type = type } - + public static func text(_ name: String) -> Column { Column(name: name, type: .text) } - + public static func integer(_ name: String) -> Column { Column(name: name, type: .integer) } - + public static func real(_ name: String) -> Column { Column(name: name, type: .real) } diff --git a/Sources/PowerSync/Protocol/Schema/Index.swift b/Sources/PowerSync/Protocol/Schema/Index.swift index 009fe83..955a27f 100644 --- a/Sources/PowerSync/Protocol/Schema/Index.swift +++ b/Sources/PowerSync/Protocol/Schema/Index.swift @@ -1,7 +1,7 @@ import Foundation import PowerSyncKotlin -public protocol IndexProtocol { +public protocol IndexProtocol: Sendable { /// /// Descriptive name of the index. /// @@ -15,7 +15,7 @@ public protocol IndexProtocol { public struct Index: IndexProtocol { public let name: String public let columns: [IndexedColumnProtocol] - + public init( name: String, columns: [IndexedColumnProtocol] @@ -23,14 +23,14 @@ public struct Index: IndexProtocol { self.name = name self.columns = columns } - + public init( name: String, _ columns: IndexedColumnProtocol... ) { self.init(name: name, columns: columns) } - + public static func ascending( name: String, columns: [String] @@ -40,7 +40,7 @@ public struct Index: IndexProtocol { columns: columns.map { IndexedColumn.ascending($0) } ) } - + public static func ascending( name: String, column: String diff --git a/Sources/PowerSync/Protocol/Schema/IndexedColumn.swift b/Sources/PowerSync/Protocol/Schema/IndexedColumn.swift index 3aee895..95a6ba1 100644 --- a/Sources/PowerSync/Protocol/Schema/IndexedColumn.swift +++ b/Sources/PowerSync/Protocol/Schema/IndexedColumn.swift @@ -3,7 +3,7 @@ import Foundation /// /// Describes an indexed column. /// -public protocol IndexedColumnProtocol { +public protocol IndexedColumnProtocol: Sendable { /// /// Name of the column to index. /// @@ -17,7 +17,7 @@ public protocol IndexedColumnProtocol { public struct IndexedColumn: IndexedColumnProtocol { public let column: String public let ascending: Bool - + public init( column: String, ascending: Bool = true @@ -25,14 +25,14 @@ public struct IndexedColumn: IndexedColumnProtocol { self.column = column self.ascending = ascending } - + /// /// Creates ascending IndexedColumn /// public static func ascending(_ column: String) -> IndexedColumn { IndexedColumn(column: column, ascending: true) } - + /// /// Creates descending IndexedColumn /// diff --git a/Sources/PowerSync/Protocol/Schema/RawTable.swift b/Sources/PowerSync/Protocol/Schema/RawTable.swift index b92e7b8..b209583 100644 --- a/Sources/PowerSync/Protocol/Schema/RawTable.swift +++ b/Sources/PowerSync/Protocol/Schema/RawTable.swift @@ -1,9 +1,9 @@ /// A table that is managed by the user instead of being auto-created and migrated by the PowerSync SDK. -/// +/// /// These tables give application developers full control over the table (including table and column constraints). /// The ``RawTable/put`` and ``RawTable/delete`` statements used by the sync client to apply /// operations to the local database also need to be set explicitly. -/// +/// /// A main benefit of raw tables is that, since they're not backed by JSON views, complex queries on them /// can be much more efficient. /// However, it's the responsibility of the developer to create these raw tables, migrate them when necessary @@ -21,19 +21,19 @@ public struct RawTable: BaseTableProtocol { /// The statement to run when the sync client has to insert or update a row. public let put: PendingStatement - + /// The statement to run when the sync client has to delete a row. public let delete: PendingStatement - + public init(name: String, put: PendingStatement, delete: PendingStatement) { - self.name = name; - self.put = put; - self.delete = delete; + self.name = name + self.put = put + self.delete = delete } } /// A statement to run to sync server-side changes into a local raw table. -public struct PendingStatement { +public struct PendingStatement: Sendable { /// The SQL statement to execute. public let sql: String /// For parameters in the prepared statement, the values to fill in. @@ -41,7 +41,7 @@ public struct PendingStatement { /// Note that the ``RawTable/delete`` statement can only use ``PendingStatementParameter/id`` - upsert /// statements can also use ``PendingStatementParameter/column`` to refer to columns. public let parameters: [PendingStatementParameter] - + public init(sql: String, parameters: [PendingStatementParameter]) { self.sql = sql self.parameters = parameters @@ -49,7 +49,7 @@ public struct PendingStatement { } /// A parameter that can be used in a ``PendingStatement``. -public enum PendingStatementParameter { +public enum PendingStatementParameter: Sendable { /// A value that resolves to the textual id of the row to insert, update or delete. case id /// A value that resolves to the value of a column in a `PUT` operation for inserts or updates. diff --git a/Sources/PowerSync/Protocol/Schema/Schema.swift b/Sources/PowerSync/Protocol/Schema/Schema.swift index 473f7db..c1e93be 100644 --- a/Sources/PowerSync/Protocol/Schema/Schema.swift +++ b/Sources/PowerSync/Protocol/Schema/Schema.swift @@ -1,9 +1,9 @@ -public protocol SchemaProtocol { +public protocol SchemaProtocol: Sendable { /// /// Tables used in Schema /// var tables: [Table] { get } - + /// Raw tables referenced in the schema. var rawTables: [RawTable] { get } /// @@ -20,13 +20,14 @@ public struct Schema: SchemaProtocol { self.tables = tables self.rawTables = rawTables } + /// /// Convenience initializer with variadic parameters /// public init(_ tables: BaseTableProtocol...) { var managedTables: [Table] = [] var rawTables: [RawTable] = [] - + for table in tables { if let table = table as? Table { managedTables.append(table) @@ -36,7 +37,7 @@ public struct Schema: SchemaProtocol { fatalError("BaseTableProtocol must only be implemented in Swift SDK") } } - + self.init(tables: managedTables, rawTables: rawTables) } diff --git a/Sources/PowerSync/Protocol/Schema/Table.swift b/Sources/PowerSync/Protocol/Schema/Table.swift index 73f983a..1d7d668 100644 --- a/Sources/PowerSync/Protocol/Schema/Table.swift +++ b/Sources/PowerSync/Protocol/Schema/Table.swift @@ -1,7 +1,7 @@ import Foundation /// Shared protocol for both PowerSync-managed ``Table``s as well as ``RawTable``s managed by the user. -public protocol BaseTableProtocol { +public protocol BaseTableProtocol: Sendable { /// /// The synced table name, matching sync rules. /// @@ -31,7 +31,7 @@ public protocol TableProtocol: BaseTableProtocol { /// var viewNameOverride: String? { get } var viewName: String { get } - + /// Whether to add a hidden `_metadata` column that will ne abled for updates to /// attach custom information about writes. /// @@ -39,12 +39,12 @@ public protocol TableProtocol: BaseTableProtocol { /// part of ``CrudEntry/opData``. Instead, it is reported as ``CrudEntry/metadata``, /// allowing ``PowerSyncBackendConnector``s to handle these updates specially. var trackMetadata: Bool { get } - + /// When set to a non-`nil` value, track old values of columns for ``CrudEntry/previousValues``. /// /// See ``TrackPreviousValuesOptions`` for details var trackPreviousValues: TrackPreviousValuesOptions? { get } - + /// Whether an `UPDATE` statement that doesn't change any values should be ignored entirely when /// creating CRUD entries. /// @@ -56,17 +56,17 @@ public protocol TableProtocol: BaseTableProtocol { /// Options to include old values in ``CrudEntry/previousValues`` for update statements. /// /// These options are enabled by passing them to a non-local ``Table`` constructor. -public struct TrackPreviousValuesOptions { +public struct TrackPreviousValuesOptions: Sendable { /// A filter of column names for which updates should be tracked. /// /// When set to a non-`nil` value, columns not included in this list will not appear in /// ``CrudEntry/previousValues``. By default, all columns are included. - public let columnFilter: [String]?; - + public let columnFilter: [String]? + /// Whether to only include old values when they were changed by an update, instead of always including /// all old values. - public let onlyWhenChanged: Bool; - + public let onlyWhenChanged: Bool + public init(columnFilter: [String]? = nil, onlyWhenChanged: Bool = false) { self.columnFilter = columnFilter self.onlyWhenChanged = onlyWhenChanged @@ -93,7 +93,7 @@ public struct Table: TableProtocol { viewNameOverride ?? name } - internal var internalName: String { + var internalName: String { localOnly ? "ps_data_local__\(name)" : "ps_data__\(name)" } @@ -125,9 +125,9 @@ public struct Table: TableProtocol { } private func hasInvalidSqliteCharacters(_ string: String) -> Bool { - let range = NSRange(location: 0, length: string.utf16.count) - return invalidSqliteCharacters.firstMatch(in: string, options: [], range: range) != nil - } + let range = NSRange(location: 0, length: string.utf16.count) + return invalidSqliteCharacters.firstMatch(in: string, options: [], range: range) != nil + } /// /// Validate the table @@ -136,12 +136,13 @@ public struct Table: TableProtocol { if columns.count > MAX_AMOUNT_OF_COLUMNS { throw TableError.tooManyColumns(tableName: name, count: columns.count) } - + if let viewNameOverride = viewNameOverride, - hasInvalidSqliteCharacters(viewNameOverride) { + hasInvalidSqliteCharacters(viewNameOverride) + { throw TableError.invalidViewName(viewName: viewNameOverride) } - + if localOnly { if trackPreviousValues != nil { throw TableError.trackPreviousForLocalTable(tableName: name) diff --git a/Sources/PowerSync/Protocol/SyncRequestLogger.swift b/Sources/PowerSync/Protocol/SyncRequestLogger.swift index 46b31ad..ca05523 100644 --- a/Sources/PowerSync/Protocol/SyncRequestLogger.swift +++ b/Sources/PowerSync/Protocol/SyncRequestLogger.swift @@ -3,7 +3,7 @@ /// 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 { +public enum SyncRequestLogLevel: Sendable { /// Log all network activity including headers, body, and info case all /// Log only request/response headers @@ -23,29 +23,29 @@ public enum SyncRequestLogLevel { /// are logged. /// /// - Note: The request level cannot be changed after initialization. A new call to `PowerSyncDatabase.connect` is required to change the level. -public struct SyncRequestLoggerConfiguration { +public struct SyncRequestLoggerConfiguration: Sendable { /// The request logging level that determines which network events are logged. /// Set once during initialization and used throughout the session. public let requestLevel: SyncRequestLogLevel - - private let logHandler: (_ message: String) -> Void - + + private let logHandler: @Sendable (_ message: String) -> Void + /// Creates a new network logger configuration. /// - Parameters: /// - requestLevel: The `SyncRequestLogLevel` to use for filtering log messages /// - logHandler: A closure which handles log messages public init( requestLevel: SyncRequestLogLevel, - logHandler: @escaping (_ message: String) -> Void) - { + logHandler: @Sendable @escaping (_ message: String) -> Void + ) { self.requestLevel = requestLevel self.logHandler = logHandler } - + public func log(_ message: String) { logHandler(message) } - + /// Creates a new network logger configuration using a `LoggerProtocol` instance. /// /// This initializer allows integration with an existing logging framework by adapting @@ -62,21 +62,21 @@ public struct SyncRequestLoggerConfiguration { requestLevel: SyncRequestLogLevel, logger: LoggerProtocol, logSeverity: LogSeverity = .debug, - logTag: String? = nil) - { + logTag: String? = nil + ) { self.requestLevel = requestLevel - self.logHandler = { message in + logHandler = { message in switch logSeverity { - case .debug: - logger.debug(message, tag: logTag) - case .info: - logger.info(message, tag: logTag) - case .warning: - logger.warning(message, tag: logTag) - case .error: - logger.error(message, tag: logTag) - case .fault: - logger.fault(message, tag: logTag) + case .debug: + logger.debug(message, tag: logTag) + case .info: + logger.info(message, tag: logTag) + case .warning: + logger.warning(message, tag: logTag) + case .error: + logger.error(message, tag: logTag) + case .fault: + logger.fault(message, tag: logTag) } } } diff --git a/Sources/PowerSync/Protocol/db/CrudBatch.swift b/Sources/PowerSync/Protocol/db/CrudBatch.swift index c57b917..6c770f4 100644 --- a/Sources/PowerSync/Protocol/db/CrudBatch.swift +++ b/Sources/PowerSync/Protocol/db/CrudBatch.swift @@ -1,7 +1,7 @@ import Foundation /// A transaction of client-side changes. -public protocol CrudBatch { +public protocol CrudBatch: Sendable { /// Indicates if there are additional Crud items in the queue which are not included in this batch var hasMore: Bool { get } diff --git a/Sources/PowerSync/Protocol/db/CrudEntry.swift b/Sources/PowerSync/Protocol/db/CrudEntry.swift index 9378262..85ecfeb 100644 --- a/Sources/PowerSync/Protocol/db/CrudEntry.swift +++ b/Sources/PowerSync/Protocol/db/CrudEntry.swift @@ -1,26 +1,26 @@ /// Represents the type of CRUD update operation that can be performed on a row. -public enum UpdateType: String, Codable { +public enum UpdateType: String, Codable, Sendable { /// A row has been inserted or replaced case put = "PUT" - + /// A row has been updated case patch = "PATCH" - + /// A row has been deleted case delete = "DELETE" - + /// Errors related to invalid `UpdateType` states. enum UpdateTypeStateError: Error { /// Indicates an invalid state with the provided string value. case invalidState(String) } - + /// Converts a string to an `UpdateType` enum value. /// - Parameter input: The string representation of the update type. /// - Throws: `UpdateTypeStateError.invalidState` if the input string does not match any `UpdateType`. /// - Returns: The corresponding `UpdateType` enum value. static func fromString(_ input: String) throws -> UpdateType { - guard let mapped = UpdateType.init(rawValue: input) else { + guard let mapped = UpdateType(rawValue: input) else { throw UpdateTypeStateError.invalidState(input) } return mapped @@ -28,22 +28,22 @@ public enum UpdateType: String, Codable { } /// Represents a CRUD (Create, Read, Update, Delete) entry in the system. -public protocol CrudEntry { +public protocol CrudEntry: Sendable { /// The unique identifier of the entry. var id: String { get } - + /// The client ID associated with the entry. var clientId: Int64 { get } - + /// The type of update operation performed on the entry. var op: UpdateType { get } - + /// The name of the table where the entry resides. var table: String { get } - + /// The transaction ID associated with the entry, if any. var transactionId: Int64? { get } - + /// User-defined metadata that can be attached to writes. /// /// This is the value the `_metadata` column had when the write to the database was made, @@ -52,7 +52,7 @@ public protocol CrudEntry { /// Note that the `_metadata` column and this field are only available when ``Table/trackMetadata`` /// is enabled. var metadata: String? { get } - + /// The operation data associated with the entry, represented as a dictionary of column names to their values. var opData: [String: String?]? { get } diff --git a/Sources/PowerSync/Protocol/db/CrudTransaction.swift b/Sources/PowerSync/Protocol/db/CrudTransaction.swift index eadc954..ace3ea4 100644 --- a/Sources/PowerSync/Protocol/db/CrudTransaction.swift +++ b/Sources/PowerSync/Protocol/db/CrudTransaction.swift @@ -1,7 +1,7 @@ import Foundation /// A transaction of client-side changes. -public protocol CrudTransaction { +public protocol CrudTransaction: Sendable { /// Unique transaction id. /// /// If nil, this contains a list of changes recorded without an explicit transaction associated. diff --git a/Sources/PowerSync/Protocol/db/JsonParam.swift b/Sources/PowerSync/Protocol/db/JsonParam.swift index a9d2835..4b0b105 100644 --- a/Sources/PowerSync/Protocol/db/JsonParam.swift +++ b/Sources/PowerSync/Protocol/db/JsonParam.swift @@ -2,47 +2,47 @@ /// /// Supports all standard JSON types: string, number (integer and double), /// boolean, null, arrays, and nested objects. -public enum JsonValue: Codable { +public enum JsonValue: Codable, Sendable { /// A JSON string value. case string(String) - + /// A JSON integer value. case int(Int) - + /// A JSON double-precision floating-point value. case double(Double) - + /// A JSON boolean value (`true` or `false`). case bool(Bool) - + /// A JSON null value. case null - + /// A JSON array containing a list of `JSONValue` elements. case array([JsonValue]) - + /// A JSON object containing key-value pairs where values are `JSONValue` instances. case object([String: JsonValue]) - + /// Converts the `JSONValue` into a native Swift representation. /// /// - Returns: A corresponding Swift type (`String`, `Int`, `Double`, `Bool`, `nil`, `[Any]`, or `[String: Any]`), /// or `nil` if the value is `.null`. func toValue() -> Any? { switch self { - case .string(let value): + case let .string(value): return value - case .int(let value): + case let .int(value): return value - case .double(let value): + case let .double(value): return value - case .bool(let value): + case let .bool(value): return value case .null: return nil - case .array(let array): + case let .array(array): return array.map { $0.toValue() } - case .object(let dict): + case let .object(dict): var anyDict: [String: Any] = [:] for (key, value) in dict { anyDict[key] = value.toValue() diff --git a/Sources/PowerSync/Protocol/sync/DownloadProgress.swift b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift index 5b114ab..9cb3020 100644 --- a/Sources/PowerSync/Protocol/sync/DownloadProgress.swift +++ b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift @@ -1,12 +1,12 @@ /// Information about a progressing download. -/// +/// /// This reports the ``totalOperations`` amount of operations to download, how many of them /// have already been downloaded as ``downloadedOperations`` and finally a ``fraction`` indicating /// relative progress. -/// +/// /// To obtain a ``ProgressWithOperations`` instance, either use ``SyncStatusData/downloadProgress`` /// for global progress or ``SyncDownloadProgress/untilPriority(priority:)``. -public protocol ProgressWithOperations { +public protocol ProgressWithOperations: Sendable { /// How many operations need to be downloaded in total for the current download /// to complete. var totalOperations: Int32 { get } @@ -18,42 +18,42 @@ public protocol ProgressWithOperations { public extension ProgressWithOperations { /// The relative amount of ``totalOperations`` to items in ``downloadedOperations``, as a /// number between `0.0` and `1.0` (inclusive). - /// + /// /// When this number reaches `1.0`, all changes have been received from the sync service. /// Actually applying these changes happens before the ``SyncStatusData/downloadProgress`` /// field is cleared though, so progress can stay at `1.0` for a short while before completing. var fraction: Float { - if (self.totalOperations == 0) { + if totalOperations == 0 { return 0.0 } - return Float.init(self.downloadedOperations) / Float.init(self.totalOperations) + return Float(downloadedOperations) / Float(totalOperations) } } /// Provides realtime progress on how PowerSync is downloading rows. -/// +/// /// This type reports progress by extending ``ProgressWithOperations``, meaning that the /// ``ProgressWithOperations/totalOperations``, ``ProgressWithOperations/downloadedOperations`` /// and ``ProgressWithOperations/fraction`` properties are available on this instance. /// Additionally, it's possible to obtain progress towards a specific priority only (instead /// of tracking progress for the entire download) by using ``untilPriority(priority:)``. -/// +/// /// The reported progress always reflects the status towards the end of a sync iteration (after /// which a consistent snapshot of all buckets is available locally). -/// +/// /// In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets) /// operation takes place between syncs), it's possible for the returned numbers to be slightly /// inaccurate. For this reason, ``SyncDownloadProgress`` should be seen as an approximation of progress. /// The information returned is good enough to build progress bars, but not exaxt enough to track /// individual download counts. -/// +/// /// Also note that data is downloaded in bulk, which means that individual counters are unlikely /// to be updated one-by-one. public protocol SyncDownloadProgress: ProgressWithOperations { /// Returns download progress towardss all data up until the specified `priority` /// being received. - /// + /// /// The returned ``ProgressWithOperations`` instance tracks the target amount of operations that /// need to be downloaded in total and how many of them have already been received. func untilPriority(priority: BucketPriority) -> ProgressWithOperations diff --git a/Sources/PowerSync/Protocol/sync/PriorityStatusEntry.swift b/Sources/PowerSync/Protocol/sync/PriorityStatusEntry.swift index be9bc4b..10e4586 100644 --- a/Sources/PowerSync/Protocol/sync/PriorityStatusEntry.swift +++ b/Sources/PowerSync/Protocol/sync/PriorityStatusEntry.swift @@ -1,7 +1,7 @@ import Foundation /// Represents the status of a bucket priority, including synchronization details. -public struct PriorityStatusEntry { +public struct PriorityStatusEntry: Sendable { /// The priority of the bucket. public let priority: BucketPriority diff --git a/Tests/PowerSyncTests/ConnectTests.swift b/Tests/PowerSyncTests/ConnectTests.swift index ab373a0..9473d21 100644 --- a/Tests/PowerSyncTests/ConnectTests.swift +++ b/Tests/PowerSyncTests/ConnectTests.swift @@ -133,4 +133,50 @@ final class ConnectTests: XCTestCase { try await database.disconnectAndClear() } + + func testSendableConnect() async throws { + /// This is just a basic sanity check to confirm that these protocols are + /// correctly defined as Sendable. + /// Declaring this struct as Sendable means all its + /// sub items should be sendable + struct SendableTest: Sendable { + let schema: Schema + let connectOptions: ConnectOptions + } + + let testOptions = SendableTest( + schema: Schema( + tables: [ + Table( + name: "users", + columns: [ + Column( + name: "name", + type: .text + ) + ] + ) + ] + ), + connectOptions: ConnectOptions( + crudThrottle: 1, + retryDelay: 1, + params: ["Name": .string("AName")], + clientConfiguration: SyncClientConfiguration( + requestLogger: SyncRequestLoggerConfiguration( + requestLevel: .all, + logger: database.logger + )) + ) + ) + + try await database.updateSchema( + schema: testOptions.schema + ) + + try await database.connect( + connector: MockConnector(), + options: testOptions.connectOptions + ) + } } diff --git a/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift b/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift index 9fae60f..ebecd44 100644 --- a/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift +++ b/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift @@ -18,7 +18,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { ) ]) - database = KotlinPowerSyncDatabaseImpl( + database = PowerSyncDatabase( schema: schema, dbFilename: ":memory:", logger: DatabaseLogger(DefaultLogger()) @@ -521,7 +521,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { try await db2.close() - let warningIndex = testWriter.logs.firstIndex( + let warningIndex = testWriter.getLogs().firstIndex( where: { value in value.contains("warning: Multiple PowerSync instances for the same database have been detected") } @@ -542,7 +542,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { try await db2.close() - let warningIndex = testWriter.logs.firstIndex( + let warningIndex = testWriter.getLogs().firstIndex( where: { value in value.contains("warning: Multiple PowerSync instances for the same database have been detected") } diff --git a/Tests/PowerSyncTests/Kotlin/TestLogger.swift b/Tests/PowerSyncTests/Kotlin/TestLogger.swift index bff9ad1..1bf5a6a 100644 --- a/Tests/PowerSyncTests/Kotlin/TestLogger.swift +++ b/Tests/PowerSyncTests/Kotlin/TestLogger.swift @@ -1,11 +1,23 @@ +import Foundation @testable import PowerSync +final class TestLogWriterAdapter: LogWriterProtocol, + // The shared state is guarded by the DispatchQueue + @unchecked Sendable +{ + private let queue = DispatchQueue(label: "TestLogWriterAdapter") + + private var logs = [String]() + + func getLogs() -> [String] { + queue.sync { + logs + } + } -class TestLogWriterAdapter: LogWriterProtocol { - var logs = [String]() - func log(severity: LogSeverity, message: String, tag: String?) { - logs.append("\(severity): \(message) \(tag != nil ? "\(tag!)" : "")") + queue.sync { + logs.append("\(severity): \(message) \(tag != nil ? "\(tag!)" : "")") + } } } -