-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathPowerSyncDatabaseImpl.swift
More file actions
244 lines (206 loc) · 8.94 KB
/
Copy pathPowerSyncDatabaseImpl.swift
File metadata and controls
244 lines (206 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import AsyncAlgorithms
import Foundation
final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
let logger: any LoggerProtocol
let group: ActiveDatabaseGroup
let syncStatus = SwiftSyncStatus()
private let dbFilename: String?
private let customHttpClient: HttpClient?
private let initializer = DatabaseInitializationAction()
let pool: any SQLiteConnectionPoolProtocol
let schema: AsyncMutex<Schema>
init(
dbFilename: String? = nil,
identifier: String,
activeInstanceStore: DatabaseGroupCollection = .shared,
logger: any LoggerProtocol,
pool: any SQLiteConnectionPoolProtocol,
customHttpClient: HttpClient?,
schema: Schema
) {
self.dbFilename = dbFilename
self.logger = logger
self.schema = AsyncMutex(schema)
self.customHttpClient = customHttpClient
self.pool = pool
self.group = activeInstanceStore.referenceGroup(identifier: identifier, logger: logger)
}
var currentStatus: any SyncStatus {
syncStatus
}
func resolveOfflineSyncStatusIfNotConnected() async throws {
try await group.syncCoordinator.guardNotConnected(inner: {
try await resolveOfflineSyncStatus()
}, ifConnected: {})
}
private func initialize() async throws {
try await initializer.ensureInitialized(db: self)
}
fileprivate func resolveOfflineSyncStatus() async throws {
// We can't use get() here because it runs as part of the initialization step.
let offlineSyncStatus = try await readLockInner { connection in
try connection.get(sql: "SELECT powersync_offline_sync_status()", parameters: []) { cursor in
let raw = try cursor.getString(index: 0)
guard let data = raw.data(using: .utf8) else {
throw PowerSyncError.operationFailed(message: "Could not encode offline sync status")
}
return try StreamingSyncClient.jsonDecoder.decode(CoreDownloadSyncStatus.self, from: data)
}
}
syncStatus.mutateStatus { $0 = MutableSyncStatus(core: offlineSyncStatus) }
}
func updateSchema(schema: any SchemaProtocol) async throws {
try await initializer.ensureInitialized(db: self)
try await group.syncCoordinator.guardNotConnected(
inner: {
let schema = Schema(other: schema)
await self.schema.withMutex { $0 = schema }
try await applySchema(schema: schema)
},
ifConnected: { throw PowerSyncError.operationFailed(message: "Cannot update schema while connected") }
)
}
fileprivate func applySchema(schema: Schema) async throws {
try await pool.withAllConnections { writer, readers in
let encoded = try StreamingSyncClient.jsonEncoder.encode(schema)
guard let asString = String(data: encoded, encoding: .utf8) else {
throw PowerSyncError.operationFailed(message: "Could not serialize schema")
}
let _ = try writer.execute(sql: "SELECT powersync_replace_schema(?)", parameters: [.string(asString)])
for reader in readers {
// Update the schema on all read connections
let _ = try reader.execute(sql: "pragma table_info('sqlite_master')", parameters: [])
}
}
}
func waitForFirstSync() async throws {
try await initialize()
await syncStatus.waitFor { $0.hasSynced == true }
}
func waitForFirstSync(priority: Int32) async throws {
try await initialize()
let priority = BucketPriority(priority)
await syncStatus.waitFor { $0.statusForPriority(priority).hasSynced == true }
}
func getPowerSyncVersion() async throws -> String {
try await initialize()
// Set during initialization
return await initializer.powerSyncVersion!
}
func disconnect() async throws {
await group.syncCoordinator.disconnect()
}
func syncStream(name: String, params: JsonParam?) -> any SyncStream {
PendingSyncStream(db: self, name: name, parameters: params)
}
func close() async throws {
try await initialize()
try await initializer.close {
await group.syncCoordinator.disconnect()
try await pool.close()
}
}
func close(deleteDatabase: Bool) async throws {
try await close()
if deleteDatabase, let dbFilename {
if dbFilename.hasPrefix("/") {
let url = URL(fileURLWithPath: dbFilename)
try deleteSQLiteFiles(dbFilename: url.lastPathComponent, in: url.deletingLastPathComponent())
} else {
let directory = try DatabaseLocation.appleDefaultDatabaseDirectory()
try deleteSQLiteFiles(dbFilename: dbFilename, in: directory)
}
}
}
func connect(connector: any PowerSyncBackendConnectorProtocol, options: ConnectOptions?) async throws {
try await initialize()
await group.syncCoordinator.connect(db: self, connector: connector, options: options ?? ConnectOptions(), client: customHttpClient)
}
func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws {
try await initialize()
try await group.syncCoordinator.disconnectAndThen {
var flags = 0
if clearLocal {
flags |= 1
}
if soft {
flags |= 2
}
do {
let flags = flags
let _ = try await writeLockInner { ctx in try ctx.execute(sql: "SELECT powersync_clear(?)", parameters: [flags]) }
}
}
}
func writeLock<R: Sendable>(callback: @escaping @Sendable (any ConnectionContext) throws -> R) async throws -> R {
try await initialize()
return try await writeLockInner(callback: callback)
}
fileprivate func writeLockInner<R: Sendable>(callback: @escaping @Sendable (any ConnectionContext) throws -> R) async throws -> R {
return try await self.pool.write { connection in
try callback(ConnectionLeaseContext(lease: connection))
}
}
func readLock<R: Sendable>(callback: @escaping @Sendable (any ConnectionContext) throws -> R) async throws -> R {
try await initialize()
return try await readLockInner(callback: callback)
}
fileprivate func readLockInner<R: Sendable>(callback: @escaping @Sendable (any ConnectionContext) throws -> R) async throws -> R {
return try await pool.read { connection in
try callback(ConnectionLeaseContext(lease: connection))
}
}
func watch<RowType: Sendable>(options: WatchOptions<RowType>) throws -> AsyncThrowingStream<[RowType], any Error> {
return watchImpl(db: self, options: options)
}
static let maxOpId = Int64.max
}
private actor DatabaseInitializationAction {
private var isInitialized = false
var powerSyncVersion: String?
private var closed = false
func ensureInitialized(db: PowerSyncDatabaseImpl) async throws {
if closed {
throw PowerSyncError.operationFailed(message: "Attempted to use closed PowerSync database")
}
if isInitialized {
return
}
powerSyncVersion = try await db.writeLockInner { conn in
let sqliteVersion = try conn.get(sql: "SELECT sqlite_version()", parameters: []) { try $0.getString(index: 0) }
let powerSyncVersion = try conn.get(sql: "SELECT powersync_rs_version()", parameters: []) { try $0.getString(index: 0) }
db.logger.debug("Opened connection. SQLite version \(sqliteVersion), PowerSync SQLite core extension \(powerSyncVersion)", tag: "PowerSyncDatabase")
try conn.execute(sql: "SELECT powersync_init()", parameters: [])
return powerSyncVersion
}
try await db.applySchema(schema: db.schema.withMutex { $0 })
try await db.resolveOfflineSyncStatus()
isInitialized = true
}
func close(action: () async throws -> ()) async rethrows {
if !closed {
closed = true
try await action()
}
}
}
func deleteSQLiteFiles(dbFilename: String, in directory: URL) throws {
let fileManager = FileManager.default
// SQLite files to delete:
// 1. Main database file: dbFilename
// 2. WAL file: dbFilename-wal
// 3. SHM file: dbFilename-shm
// 4. Journal file: dbFilename-journal (for rollback journal mode, though WAL mode typically doesn't use it)
let filesToDelete = [
dbFilename,
"\(dbFilename)-wal",
"\(dbFilename)-shm",
"\(dbFilename)-journal"
]
for filename in filesToDelete {
let fileUrl = directory.appendingPathComponent(filename)
if fileManager.fileExists(atPath: fileUrl.path) {
try fileManager.removeItem(at: fileUrl)
}
}
}