@trainstar/synchro-react-native 0.4.0-rc.3 → 0.4.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -5
- package/SynchroReactNative.podspec +2 -1
- package/android/build.gradle +8 -7
- package/ios/SynchroModule.swift +776 -540
- package/ios/SynchroOwnership.swift +173 -0
- package/ios-test-host/{main.m → AppDelegate.m} +0 -7
- package/ios-test-host/main.swift +12 -0
- package/ios-tests/SynchroModuleTransactionTests.swift +93 -72
- package/ios-tests/TransactionSessionTests.swift +167 -12
- package/package.json +4 -1
- package/tooling-patches/apply.cjs +82 -1
- package/tooling-patches/react-native-gradle-plugin-0.82.1-kotlin-2.4.21.patch +131 -0
- package/tooling-patches/react-native-gradle-plugin-0.88.0-kotlin-2.4.21.patch +30 -0
package/ios/SynchroModule.swift
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Darwin
|
|
3
|
+
import os
|
|
3
4
|
import React
|
|
4
5
|
import GRDB
|
|
5
6
|
@_spi(Inspection) import Synchro
|
|
@@ -12,166 +13,220 @@ import GRDB
|
|
|
12
13
|
|
|
13
14
|
// MARK: - Transaction Session
|
|
14
15
|
|
|
15
|
-
class TransactionSession {
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
final class TransactionSession: Sendable {
|
|
17
|
+
private struct State: Sendable {
|
|
18
|
+
var operations: [TransactionOp] = []
|
|
19
|
+
var closed = false
|
|
20
|
+
var abortError: Error?
|
|
21
|
+
var finalSettlement: TransactionFinalSettlement?
|
|
22
|
+
var finished = false
|
|
23
|
+
var finishWaiters: [CheckedContinuation<Void, Never>] = []
|
|
24
|
+
var beginSettlement: ReactPromiseSettlement?
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private struct BeginAction: Sendable {
|
|
28
|
+
let settlement: ReactPromiseSettlement?
|
|
29
|
+
let error: Error?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private enum NextAction: Sendable {
|
|
33
|
+
case operation(TransactionOp)
|
|
34
|
+
case closed
|
|
35
|
+
case aborted(Error)
|
|
36
|
+
case wait
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private let state: OSAllocatedUnfairLock<State>
|
|
40
|
+
private let wake = DispatchSemaphore(value: 0)
|
|
24
41
|
let isWrite: Bool
|
|
25
42
|
|
|
26
|
-
init(isWrite: Bool,
|
|
43
|
+
init(isWrite: Bool, beginSettlement: ReactPromiseSettlement) {
|
|
27
44
|
self.isWrite = isWrite
|
|
28
|
-
|
|
45
|
+
state = OSAllocatedUnfairLock(initialState: State(beginSettlement: beginSettlement))
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
/// Settles begin when the native transaction callback starts. A close that
|
|
32
49
|
/// aborted the session before the callback started rejects begin instead.
|
|
33
50
|
func acceptBegin(_ txID: String) throws {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if let error {
|
|
40
|
-
|
|
51
|
+
let action = state.withLock { state -> BeginAction in
|
|
52
|
+
let settlement = state.beginSettlement
|
|
53
|
+
state.beginSettlement = nil
|
|
54
|
+
return BeginAction(settlement: settlement, error: state.abortError)
|
|
55
|
+
}
|
|
56
|
+
if let error = action.error {
|
|
57
|
+
if let settlement = action.settlement {
|
|
58
|
+
SynchroModuleImpl.rejectWithError(settlement, error)
|
|
59
|
+
}
|
|
41
60
|
throw error
|
|
42
61
|
}
|
|
43
|
-
|
|
62
|
+
action.settlement?.resolve(.value(.string(txID)))
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
/// Rejects begin when the transaction fails before its callback starts.
|
|
47
66
|
func rejectBegin(_ error: Error) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
let settlement = state.withLock { state in
|
|
68
|
+
defer { state.beginSettlement = nil }
|
|
69
|
+
return state.beginSettlement
|
|
70
|
+
}
|
|
71
|
+
if let settlement {
|
|
72
|
+
SynchroModuleImpl.rejectWithError(settlement, error)
|
|
73
|
+
}
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
func enqueue(_ op: TransactionOp) -> Bool {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
77
|
+
let accepted = state.withLock { state -> Bool in
|
|
78
|
+
guard !state.closed else { return false }
|
|
79
|
+
state.operations.append(op)
|
|
80
|
+
return true
|
|
60
81
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
if accepted {
|
|
83
|
+
wake.signal()
|
|
84
|
+
}
|
|
85
|
+
return accepted
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
func nextOperation(timeout: TimeInterval) throws -> TransactionOp? {
|
|
67
|
-
condition.lock()
|
|
68
|
-
defer { condition.unlock() }
|
|
69
|
-
|
|
70
89
|
let deadline = Date().addingTimeInterval(timeout)
|
|
71
|
-
while
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
while true {
|
|
91
|
+
let action = state.withLock { state -> NextAction in
|
|
92
|
+
if let error = state.abortError {
|
|
93
|
+
return .aborted(error)
|
|
94
|
+
}
|
|
95
|
+
if !state.operations.isEmpty {
|
|
96
|
+
return .operation(state.operations.removeFirst())
|
|
97
|
+
}
|
|
98
|
+
return state.closed ? .closed : .wait
|
|
75
99
|
}
|
|
76
|
-
}
|
|
77
100
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
switch action {
|
|
102
|
+
case .operation(let operation):
|
|
103
|
+
return operation
|
|
104
|
+
case .closed:
|
|
105
|
+
return nil
|
|
106
|
+
case .aborted(let error):
|
|
107
|
+
throw error
|
|
108
|
+
case .wait:
|
|
109
|
+
let remaining = deadline.timeIntervalSinceNow
|
|
110
|
+
let timedOut = remaining <= 0
|
|
111
|
+
|| wake.wait(timeout: .now() + remaining) == .timedOut
|
|
112
|
+
guard timedOut else { continue }
|
|
113
|
+
let didTimeout = state.withLock { state -> Bool in
|
|
114
|
+
guard state.operations.isEmpty, !state.closed else { return false }
|
|
115
|
+
state.closed = true
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
if didTimeout {
|
|
119
|
+
throw TransactionTimeoutError()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
83
122
|
}
|
|
84
|
-
return operations.removeFirst()
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
@discardableResult
|
|
88
126
|
func close() -> Bool {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
condition.unlock()
|
|
127
|
+
let pending = state.withLock { state -> [TransactionOp]? in
|
|
128
|
+
guard !state.closed else { return nil }
|
|
129
|
+
state.closed = true
|
|
130
|
+
let pending = state.operations
|
|
131
|
+
state.operations.removeAll()
|
|
132
|
+
return pending
|
|
133
|
+
}
|
|
134
|
+
guard let pending else { return false }
|
|
135
|
+
wake.signal()
|
|
99
136
|
let error = TransactionAbortedError(message: "Transaction already completed")
|
|
100
137
|
pending.forEach { $0.fail(error) }
|
|
101
138
|
return true
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
func abort(_ error: Error) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
condition.unlock()
|
|
142
|
+
let pending = state.withLock { state -> [TransactionOp]? in
|
|
143
|
+
guard !state.closed else { return nil }
|
|
144
|
+
state.closed = true
|
|
145
|
+
state.abortError = error
|
|
146
|
+
let pending = state.operations
|
|
147
|
+
state.operations.removeAll()
|
|
148
|
+
return pending
|
|
149
|
+
}
|
|
150
|
+
guard let pending else { return }
|
|
151
|
+
wake.signal()
|
|
116
152
|
pending.forEach { $0.fail(error) }
|
|
117
153
|
}
|
|
118
154
|
|
|
119
155
|
func currentAbortError() -> Error? {
|
|
120
|
-
|
|
121
|
-
defer { condition.unlock() }
|
|
122
|
-
return abortError
|
|
156
|
+
state.withLock { $0.abortError }
|
|
123
157
|
}
|
|
124
158
|
|
|
125
|
-
func
|
|
126
|
-
|
|
127
|
-
finalCompletion = completion
|
|
128
|
-
condition.unlock()
|
|
159
|
+
func setFinalSettlement(_ settlement: TransactionFinalSettlement) {
|
|
160
|
+
state.withLock { $0.finalSettlement = settlement }
|
|
129
161
|
}
|
|
130
162
|
|
|
131
163
|
func completeFinal(_ result: Result<Void, Error>) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
condition.unlock()
|
|
135
|
-
completion?(result)
|
|
164
|
+
let settlement = state.withLock { $0.finalSettlement }
|
|
165
|
+
settlement?.complete(result)
|
|
136
166
|
}
|
|
137
167
|
|
|
138
168
|
func markFinished() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
let waiters = state.withLock { state -> [CheckedContinuation<Void, Never>] in
|
|
170
|
+
state.finished = true
|
|
171
|
+
let waiters = state.finishWaiters
|
|
172
|
+
state.finishWaiters.removeAll()
|
|
173
|
+
return waiters
|
|
174
|
+
}
|
|
144
175
|
waiters.forEach { $0.resume() }
|
|
145
176
|
}
|
|
146
177
|
|
|
147
178
|
func waitUntilFinished() async {
|
|
148
179
|
await withCheckedContinuation { continuation in
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
let resumeNow = state.withLock { state -> Bool in
|
|
181
|
+
if state.finished {
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
state.finishWaiters.append(continuation)
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
if resumeNow {
|
|
152
188
|
continuation.resume()
|
|
153
|
-
} else {
|
|
154
|
-
finishWaiters.append(continuation)
|
|
155
|
-
condition.unlock()
|
|
156
189
|
}
|
|
157
190
|
}
|
|
158
191
|
}
|
|
159
192
|
}
|
|
160
193
|
|
|
161
|
-
enum
|
|
162
|
-
case
|
|
163
|
-
case
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
enum TransactionFinalSettlement: Sendable {
|
|
195
|
+
case commit(ReactPromiseSettlement)
|
|
196
|
+
case rollback(ReactPromiseSettlement)
|
|
197
|
+
|
|
198
|
+
func complete(_ result: Result<Void, Error>) {
|
|
199
|
+
switch (self, result) {
|
|
200
|
+
case (.commit(let settlement), .success), (.rollback(let settlement), .success):
|
|
201
|
+
settlement.resolve(.void)
|
|
202
|
+
case (.commit(let settlement), .failure(let error as SynchroError)):
|
|
203
|
+
SynchroModuleImpl.rejectWithError(settlement, error)
|
|
204
|
+
case (.commit(let settlement), .failure(let error)),
|
|
205
|
+
(.rollback(let settlement), .failure(let error)):
|
|
206
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
enum TransactionOp: Sendable {
|
|
212
|
+
case query(sql: String, params: Result<[BridgeValue], Error>, settlement: ReactPromiseSettlement)
|
|
213
|
+
case queryOne(sql: String, params: Result<[BridgeValue], Error>, settlement: ReactPromiseSettlement)
|
|
214
|
+
case execute(sql: String, params: Result<[BridgeValue], Error>, settlement: ReactPromiseSettlement)
|
|
215
|
+
case commit(settlement: ReactPromiseSettlement)
|
|
216
|
+
case rollback(settlement: ReactPromiseSettlement)
|
|
167
217
|
|
|
168
218
|
func fail(_ error: Error) {
|
|
219
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private var settlement: ReactPromiseSettlement {
|
|
169
223
|
switch self {
|
|
170
|
-
case .query(_, _, let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
224
|
+
case .query(_, _, let settlement),
|
|
225
|
+
.queryOne(_, _, let settlement),
|
|
226
|
+
.execute(_, _, let settlement),
|
|
227
|
+
.commit(let settlement),
|
|
228
|
+
.rollback(let settlement):
|
|
229
|
+
return settlement
|
|
175
230
|
}
|
|
176
231
|
}
|
|
177
232
|
}
|
|
@@ -216,53 +271,84 @@ private actor LifecycleMutex {
|
|
|
216
271
|
|
|
217
272
|
@objc(SynchroModuleImpl)
|
|
218
273
|
public class SynchroModuleImpl: NSObject {
|
|
219
|
-
|
|
274
|
+
private struct RuntimeState: Sendable {
|
|
275
|
+
var client: SynchroClient?
|
|
276
|
+
var sessions: [String: TransactionSession] = [:]
|
|
277
|
+
var acceptingTransactions = false
|
|
278
|
+
var observers: [String: any Synchro.Cancellable] = [:]
|
|
279
|
+
var statusSubscription: (any Synchro.Cancellable)?
|
|
280
|
+
var syncEventSubscription: (any Synchro.Cancellable)?
|
|
281
|
+
var conflictSubscription: (any Synchro.Cancellable)?
|
|
282
|
+
var cachedBackoff: SyncBackoffEvent?
|
|
283
|
+
var cachedFailure: SyncFailure?
|
|
284
|
+
var transportObservations: TransportObservationCollector?
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private struct DetachedState: Sendable {
|
|
288
|
+
let sessions: [TransactionSession]
|
|
289
|
+
let observers: [any Synchro.Cancellable]
|
|
290
|
+
let subscriptions: [any Synchro.Cancellable]
|
|
291
|
+
let transportObservations: TransportObservationCollector?
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private let eventTarget = ReactEventTarget()
|
|
295
|
+
private let runtime = OSAllocatedUnfairLock(initialState: RuntimeState())
|
|
296
|
+
private let authRequests = AuthRequestStore()
|
|
220
297
|
|
|
221
|
-
|
|
298
|
+
@objc public var eventDelegate: SynchroEventEmitting? {
|
|
299
|
+
get { eventTarget.currentDelegate }
|
|
300
|
+
set { eventTarget.setDelegate(newValue) }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
var client: SynchroClient? {
|
|
304
|
+
runtime.withLock { $0.client }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
var activeTransactionIDs: [String] {
|
|
308
|
+
runtime.withLock { Array($0.sessions.keys) }
|
|
309
|
+
}
|
|
222
310
|
private let lifecycleMutex = LifecycleMutex()
|
|
223
311
|
private let databaseBridgeQueue = DispatchQueue(label: "com.trainstar.synchro.react-native.database")
|
|
224
|
-
var sessions: [String: TransactionSession] = [:]
|
|
225
|
-
let sessionsLock = NSLock()
|
|
226
|
-
private var acceptingTransactions = false
|
|
227
|
-
private var observers: [String: any Synchro.Cancellable] = [:]
|
|
228
|
-
private var statusSubscription: (any Synchro.Cancellable)?
|
|
229
|
-
private var syncEventSubscription: (any Synchro.Cancellable)?
|
|
230
|
-
private var conflictSubscription: (any Synchro.Cancellable)?
|
|
231
|
-
private let statusDetailsLock = NSLock()
|
|
232
|
-
private var cachedBackoff: SyncBackoffEvent?
|
|
233
|
-
private var cachedFailure: SyncFailure?
|
|
234
|
-
private var transportObservations: TransportObservationCollector?
|
|
235
312
|
|
|
236
|
-
|
|
237
|
-
|
|
313
|
+
@objc public func rejectWithError(_ reject: @escaping RCTPromiseRejectBlock, _ error: Error) {
|
|
314
|
+
Self.rejectWithError(reject, error)
|
|
315
|
+
}
|
|
238
316
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
317
|
+
fileprivate static func rejectWithError(_ settlement: ReactPromiseSettlement, _ error: Error) {
|
|
318
|
+
let rejection = rejection(for: error)
|
|
319
|
+
settlement.reject(rejection.code, rejection.message, rejection.error)
|
|
243
320
|
}
|
|
244
321
|
|
|
245
|
-
|
|
322
|
+
private static func rejectWithError(_ reject: RCTPromiseRejectBlock, _ error: Error) {
|
|
323
|
+
let rejection = rejection(for: error)
|
|
324
|
+
reject(rejection.code, rejection.message, rejection.error)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private static func rejection(for error: Error) -> (code: String?, message: String?, error: Error?) {
|
|
246
328
|
if let synchroError = error as? SynchroError {
|
|
247
329
|
let (code, userInfo) = mapSynchroError(synchroError)
|
|
248
330
|
let base: [String: Any] = ["message": synchroError.localizedDescription]
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
331
|
+
return (
|
|
332
|
+
code,
|
|
333
|
+
synchroError.localizedDescription,
|
|
334
|
+
NSError(
|
|
335
|
+
domain: "SynchroModule",
|
|
336
|
+
code: 0,
|
|
337
|
+
userInfo: base.merging(userInfo) { _, new in new }
|
|
338
|
+
)
|
|
339
|
+
)
|
|
254
340
|
} else if error is TransactionTimeoutError {
|
|
255
|
-
|
|
341
|
+
return ("TRANSACTION_TIMEOUT", "Transaction timed out due to inactivity", nil)
|
|
256
342
|
} else if error is TransactionAbortedError {
|
|
257
|
-
|
|
343
|
+
return ("NOT_CONNECTED", "Client closed during transaction", error)
|
|
258
344
|
} else if error is InvalidAuthoredWriteOperationError {
|
|
259
|
-
|
|
345
|
+
return ("INVALID_CONFIG", "Invalid authored write operation", nil)
|
|
260
346
|
} else {
|
|
261
|
-
|
|
347
|
+
return ("UNKNOWN", error.localizedDescription, error)
|
|
262
348
|
}
|
|
263
349
|
}
|
|
264
350
|
|
|
265
|
-
private func mapSynchroError(_ error: SynchroError) -> (String, [String: Any]) {
|
|
351
|
+
private static func mapSynchroError(_ error: SynchroError) -> (String, [String: Any]) {
|
|
266
352
|
switch error {
|
|
267
353
|
case .notConnected:
|
|
268
354
|
return ("NOT_CONNECTED", [:])
|
|
@@ -320,16 +406,17 @@ public class SynchroModuleImpl: NSObject {
|
|
|
320
406
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
321
407
|
reject: @escaping RCTPromiseRejectBlock
|
|
322
408
|
) {
|
|
409
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
323
410
|
guard let dbPath = config["dbPath"] as? String,
|
|
324
411
|
let serverURL = config["serverURL"] as? String,
|
|
325
412
|
let clientID = config["clientID"] as? String,
|
|
326
413
|
let platform = config["platform"] as? String,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
reject("INVALID_CONFIG", "Missing required config fields", nil)
|
|
414
|
+
let appVersion = config["appVersion"] as? String,
|
|
415
|
+
let url = URL(string: serverURL) else {
|
|
416
|
+
settlement.reject("INVALID_CONFIG", "Missing required config fields", nil)
|
|
330
417
|
return
|
|
331
418
|
}
|
|
332
|
-
let resolvedDbPath = resolveDatabasePath(dbPath)
|
|
419
|
+
let resolvedDbPath = Self.resolveDatabasePath(dbPath)
|
|
333
420
|
|
|
334
421
|
let syncInterval = config["syncInterval"] as? Double ?? 30
|
|
335
422
|
let pushDebounce = config["pushDebounce"] as? Double ?? 0.5
|
|
@@ -340,7 +427,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
340
427
|
let configuredTransportCapacity = config["transportObservationCapacity"] as? Int ?? 0
|
|
341
428
|
let requireNewDatabase = config["requireNewDatabase"] as? Bool ?? false
|
|
342
429
|
if configuredTransportCapacity < 0 || configuredTransportCapacity > 512 {
|
|
343
|
-
reject("INVALID_CONFIG", "Transport observation capacity is invalid", nil)
|
|
430
|
+
settlement.reject("INVALID_CONFIG", "Transport observation capacity is invalid", nil)
|
|
344
431
|
return
|
|
345
432
|
}
|
|
346
433
|
let transportCapacity = configuredTransportCapacity == 0 ? nil : configuredTransportCapacity
|
|
@@ -366,16 +453,24 @@ public class SynchroModuleImpl: NSObject {
|
|
|
366
453
|
}
|
|
367
454
|
|
|
368
455
|
let transportObservations = transportCapacity.map(TransportObservationCollector.init(capacity:))
|
|
456
|
+
let authRequests = authRequests
|
|
457
|
+
let eventTarget = eventTarget
|
|
369
458
|
let synchroConfig = SynchroConfig(
|
|
370
459
|
dbPath: resolvedDbPath,
|
|
371
460
|
serverURL: url,
|
|
372
|
-
authProvider: {
|
|
461
|
+
authProvider: {
|
|
373
462
|
try await withCheckedThrowingContinuation { continuation in
|
|
374
463
|
let requestID = UUID().uuidString
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
464
|
+
guard authRequests.insert(continuation, for: requestID) else {
|
|
465
|
+
continuation.resume(throwing: TransactionAbortedError(
|
|
466
|
+
message: "Client closed during authentication"
|
|
467
|
+
))
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
eventTarget.emit(
|
|
471
|
+
"onAuthRequest",
|
|
472
|
+
body: .object(["requestID": .string(requestID)])
|
|
473
|
+
)
|
|
379
474
|
}
|
|
380
475
|
},
|
|
381
476
|
clientID: clientID,
|
|
@@ -389,33 +484,43 @@ public class SynchroModuleImpl: NSObject {
|
|
|
389
484
|
seedDatabasePath: resolvedSeedPath,
|
|
390
485
|
transportObservationCollector: transportObservations
|
|
391
486
|
)
|
|
392
|
-
|
|
393
|
-
|
|
487
|
+
let lifecycleMutex = lifecycleMutex
|
|
488
|
+
let runtime = runtime
|
|
489
|
+
let databaseBridgeQueue = databaseBridgeQueue
|
|
490
|
+
Task {
|
|
394
491
|
do {
|
|
395
|
-
try await
|
|
396
|
-
await
|
|
397
|
-
|
|
492
|
+
try await Self.withLifecycleLock(lifecycleMutex) {
|
|
493
|
+
await Self.clearRuntimeState(
|
|
494
|
+
runtime: runtime,
|
|
495
|
+
authRequests: authRequests,
|
|
496
|
+
databaseBridgeQueue: databaseBridgeQueue
|
|
497
|
+
)
|
|
498
|
+
try await runtime.withLock { $0.client }?.close()
|
|
398
499
|
if requireNewDatabase && FileManager.default.fileExists(atPath: resolvedDbPath) {
|
|
399
500
|
throw DatabaseAlreadyExistsError()
|
|
400
501
|
}
|
|
401
502
|
let client = try SynchroClient(config: synchroConfig)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
503
|
+
authRequests.activate()
|
|
504
|
+
runtime.withLock {
|
|
505
|
+
$0.client = client
|
|
506
|
+
$0.acceptingTransactions = true
|
|
507
|
+
$0.transportObservations = transportObservations
|
|
508
|
+
}
|
|
509
|
+
Self.wireClientEvents(client, runtime: runtime, eventTarget: eventTarget)
|
|
405
510
|
}
|
|
406
511
|
DispatchQueue.main.async {
|
|
407
|
-
resolve(
|
|
512
|
+
settlement.resolve(.void)
|
|
408
513
|
}
|
|
409
514
|
} catch is DatabaseAlreadyExistsError {
|
|
410
515
|
DispatchQueue.main.async {
|
|
411
|
-
reject("INVALID_CONFIG", "Database already exists", nil)
|
|
516
|
+
settlement.reject("INVALID_CONFIG", "Database already exists", nil)
|
|
412
517
|
}
|
|
413
518
|
} catch {
|
|
414
519
|
DispatchQueue.main.async {
|
|
415
520
|
if resolvedSeedPath != nil {
|
|
416
|
-
reject("INVALID_SEED", "Seed database failed validation", error)
|
|
521
|
+
settlement.reject("INVALID_SEED", "Seed database failed validation", error)
|
|
417
522
|
} else {
|
|
418
|
-
|
|
523
|
+
Self.rejectWithError(settlement, error)
|
|
419
524
|
}
|
|
420
525
|
}
|
|
421
526
|
}
|
|
@@ -424,18 +529,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
424
529
|
|
|
425
530
|
@objc
|
|
426
531
|
public func resolveAuthRequest(_ requestID: String, token: String) {
|
|
427
|
-
|
|
428
|
-
let continuation = pendingAuthContinuations.removeValue(forKey: requestID)
|
|
429
|
-
authLock.unlock()
|
|
430
|
-
continuation?.resume(returning: token)
|
|
532
|
+
authRequests.resolve(requestID, token: token)
|
|
431
533
|
}
|
|
432
534
|
|
|
433
535
|
@objc
|
|
434
536
|
public func rejectAuthRequest(_ requestID: String, error: String) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
537
|
+
authRequests.reject(
|
|
538
|
+
requestID,
|
|
539
|
+
error: NSError(domain: "Auth", code: 0, userInfo: [NSLocalizedDescriptionKey: error])
|
|
540
|
+
)
|
|
439
541
|
}
|
|
440
542
|
|
|
441
543
|
@objc
|
|
@@ -443,20 +545,31 @@ public class SynchroModuleImpl: NSObject {
|
|
|
443
545
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
444
546
|
reject: @escaping RCTPromiseRejectBlock
|
|
445
547
|
) {
|
|
446
|
-
|
|
447
|
-
|
|
548
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
549
|
+
let lifecycleMutex = lifecycleMutex
|
|
550
|
+
let runtime = runtime
|
|
551
|
+
let authRequests = authRequests
|
|
552
|
+
let databaseBridgeQueue = databaseBridgeQueue
|
|
553
|
+
Task {
|
|
448
554
|
do {
|
|
449
|
-
try await
|
|
450
|
-
await
|
|
451
|
-
|
|
452
|
-
|
|
555
|
+
try await Self.withLifecycleLock(lifecycleMutex) {
|
|
556
|
+
await Self.clearRuntimeState(
|
|
557
|
+
runtime: runtime,
|
|
558
|
+
authRequests: authRequests,
|
|
559
|
+
databaseBridgeQueue: databaseBridgeQueue
|
|
560
|
+
)
|
|
561
|
+
try await runtime.withLock { $0.client }?.close()
|
|
562
|
+
runtime.withLock {
|
|
563
|
+
$0.client = nil
|
|
564
|
+
$0.acceptingTransactions = false
|
|
565
|
+
}
|
|
453
566
|
}
|
|
454
567
|
DispatchQueue.main.async {
|
|
455
|
-
resolve(
|
|
568
|
+
settlement.resolve(.void)
|
|
456
569
|
}
|
|
457
570
|
} catch {
|
|
458
571
|
DispatchQueue.main.async {
|
|
459
|
-
|
|
572
|
+
Self.rejectWithError(settlement, error)
|
|
460
573
|
}
|
|
461
574
|
}
|
|
462
575
|
}
|
|
@@ -467,14 +580,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
467
580
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
468
581
|
reject: @escaping RCTPromiseRejectBlock
|
|
469
582
|
) {
|
|
470
|
-
guard let client = client else {
|
|
583
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
471
584
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
472
585
|
return
|
|
473
586
|
}
|
|
474
587
|
resolve(client.path)
|
|
475
588
|
}
|
|
476
589
|
|
|
477
|
-
private func resolveDatabasePath(_ dbPath: String) -> String {
|
|
590
|
+
private static func resolveDatabasePath(_ dbPath: String) -> String {
|
|
478
591
|
if (dbPath as NSString).isAbsolutePath {
|
|
479
592
|
return dbPath
|
|
480
593
|
}
|
|
@@ -491,11 +604,12 @@ public class SynchroModuleImpl: NSObject {
|
|
|
491
604
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
492
605
|
reject: @escaping RCTPromiseRejectBlock
|
|
493
606
|
) {
|
|
607
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
494
608
|
submitDatabaseOperation(
|
|
495
609
|
resolve: resolve,
|
|
496
610
|
reject: reject,
|
|
497
611
|
operation: { client in
|
|
498
|
-
try
|
|
612
|
+
.value(try Self.rowsToBridgeValue(client.query(sql, params: Self.bridgeParams(capturedParams.get()))))
|
|
499
613
|
}
|
|
500
614
|
)
|
|
501
615
|
}
|
|
@@ -507,14 +621,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
507
621
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
508
622
|
reject: @escaping RCTPromiseRejectBlock
|
|
509
623
|
) {
|
|
624
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
510
625
|
submitDatabaseOperation(
|
|
511
626
|
resolve: resolve,
|
|
512
627
|
reject: reject,
|
|
513
|
-
operation: { client
|
|
514
|
-
if let row = try client.queryOne(sql, params:
|
|
515
|
-
return
|
|
628
|
+
operation: { client in
|
|
629
|
+
if let row = try client.queryOne(sql, params: Self.bridgeParams(capturedParams.get())) {
|
|
630
|
+
return .value(Self.rowToBridgeValue(row))
|
|
516
631
|
}
|
|
517
|
-
return
|
|
632
|
+
return .value(.null)
|
|
518
633
|
}
|
|
519
634
|
)
|
|
520
635
|
}
|
|
@@ -526,12 +641,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
526
641
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
527
642
|
reject: @escaping RCTPromiseRejectBlock
|
|
528
643
|
) {
|
|
644
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
529
645
|
submitDatabaseOperation(
|
|
530
646
|
resolve: resolve,
|
|
531
647
|
reject: reject,
|
|
532
648
|
operation: { client in
|
|
533
|
-
let result = try client.execute(sql, params:
|
|
534
|
-
return ["rowsAffected": result.rowsAffected]
|
|
649
|
+
let result = try client.execute(sql, params: Self.bridgeParams(capturedParams.get()))
|
|
650
|
+
return .value(.object(["rowsAffected": .integer(Int64(result.rowsAffected))]))
|
|
535
651
|
}
|
|
536
652
|
)
|
|
537
653
|
}
|
|
@@ -546,6 +662,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
546
662
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
547
663
|
reject: @escaping RCTPromiseRejectBlock
|
|
548
664
|
) {
|
|
665
|
+
let capturedValues = Result { try Self.captureSQLParams(values) }
|
|
549
666
|
submitDatabaseOperation(
|
|
550
667
|
resolve: resolve,
|
|
551
668
|
reject: reject,
|
|
@@ -553,7 +670,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
553
670
|
guard let authoredOperation = Synchro.Operation(rawValue: operation) else {
|
|
554
671
|
throw InvalidAuthoredWriteOperationError()
|
|
555
672
|
}
|
|
556
|
-
let params = try
|
|
673
|
+
let params = try Self.bridgeParams(capturedValues.get())
|
|
557
674
|
let result = try client.authoredWriteTransaction(
|
|
558
675
|
tableName: tableName,
|
|
559
676
|
operation: authoredOperation,
|
|
@@ -561,7 +678,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
561
678
|
) { transaction in
|
|
562
679
|
try transaction.execute(sql, params: params)
|
|
563
680
|
}
|
|
564
|
-
return ["rowsAffected": result.rowsAffected]
|
|
681
|
+
return .value(.object(["rowsAffected": .integer(Int64(result.rowsAffected))]))
|
|
565
682
|
}
|
|
566
683
|
)
|
|
567
684
|
}
|
|
@@ -572,12 +689,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
572
689
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
573
690
|
reject: @escaping RCTPromiseRejectBlock
|
|
574
691
|
) {
|
|
692
|
+
let capturedStatements = Result { try Self.captureBridgeStatements(statements) }
|
|
575
693
|
submitDatabaseOperation(
|
|
576
694
|
resolve: resolve,
|
|
577
695
|
reject: reject,
|
|
578
696
|
operation: { client in
|
|
579
|
-
let total = try client.executeBatch(try
|
|
580
|
-
return ["totalRowsAffected": total]
|
|
697
|
+
let total = try client.executeBatch(try Self.bridgeStatements(capturedStatements.get()))
|
|
698
|
+
return .value(.object(["totalRowsAffected": .integer(Int64(total))]))
|
|
581
699
|
}
|
|
582
700
|
)
|
|
583
701
|
}
|
|
@@ -608,7 +726,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
608
726
|
beginTransaction(isWrite: false, resolve: resolve, reject: reject)
|
|
609
727
|
}
|
|
610
728
|
|
|
611
|
-
private func runTransactionLoop(
|
|
729
|
+
private static func runTransactionLoop(
|
|
612
730
|
session: TransactionSession,
|
|
613
731
|
txID: String,
|
|
614
732
|
query: (String, [(any DatabaseValueConvertible)?]) throws -> [Row],
|
|
@@ -623,60 +741,60 @@ public class SynchroModuleImpl: NSObject {
|
|
|
623
741
|
}
|
|
624
742
|
|
|
625
743
|
switch op {
|
|
626
|
-
case .query(let sql, let params, let
|
|
744
|
+
case .query(let sql, let params, let settlement):
|
|
627
745
|
do {
|
|
628
|
-
let rows = try query(sql, try bridgeParams(params))
|
|
746
|
+
let rows = try query(sql, try bridgeParams(params.get()))
|
|
629
747
|
if let abortError = session.currentAbortError() {
|
|
630
748
|
throw abortError
|
|
631
749
|
}
|
|
632
|
-
|
|
750
|
+
settlement.resolve(.value(rowsToBridgeValue(rows)))
|
|
633
751
|
} catch {
|
|
634
|
-
|
|
752
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
635
753
|
}
|
|
636
754
|
|
|
637
|
-
case .queryOne(let sql, let params, let
|
|
755
|
+
case .queryOne(let sql, let params, let settlement):
|
|
638
756
|
do {
|
|
639
|
-
let row = try queryOne(sql, try bridgeParams(params))
|
|
757
|
+
let row = try queryOne(sql, try bridgeParams(params.get()))
|
|
640
758
|
if let abortError = session.currentAbortError() {
|
|
641
759
|
throw abortError
|
|
642
760
|
}
|
|
643
|
-
|
|
761
|
+
settlement.resolve(.value(row.map(rowToBridgeValue) ?? .null))
|
|
644
762
|
} catch {
|
|
645
|
-
|
|
763
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
646
764
|
}
|
|
647
765
|
|
|
648
|
-
case .execute(let sql, let params, let
|
|
766
|
+
case .execute(let sql, let params, let settlement):
|
|
649
767
|
do {
|
|
650
768
|
guard let execute else {
|
|
651
769
|
throw TransactionReadOnlyError()
|
|
652
770
|
}
|
|
653
|
-
let rowsAffected = try execute(sql, try bridgeParams(params))
|
|
771
|
+
let rowsAffected = try execute(sql, try bridgeParams(params.get()))
|
|
654
772
|
if let abortError = session.currentAbortError() {
|
|
655
773
|
throw abortError
|
|
656
774
|
}
|
|
657
|
-
|
|
775
|
+
settlement.resolve(.value(.object(["rowsAffected": .integer(Int64(rowsAffected))])))
|
|
658
776
|
} catch {
|
|
659
|
-
|
|
777
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
660
778
|
}
|
|
661
779
|
|
|
662
|
-
case .commit(let
|
|
780
|
+
case .commit(let settlement):
|
|
663
781
|
guard session.close() else {
|
|
664
782
|
let error = session.currentAbortError()
|
|
665
783
|
?? TransactionAbortedError(message: "Transaction already completed")
|
|
666
|
-
|
|
784
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
667
785
|
throw error
|
|
668
786
|
}
|
|
669
|
-
session.
|
|
787
|
+
session.setFinalSettlement(.commit(settlement))
|
|
670
788
|
return
|
|
671
789
|
|
|
672
|
-
case .rollback(let
|
|
790
|
+
case .rollback(let settlement):
|
|
673
791
|
guard session.close() else {
|
|
674
792
|
let error = session.currentAbortError()
|
|
675
793
|
?? TransactionAbortedError(message: "Transaction already completed")
|
|
676
|
-
|
|
794
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
677
795
|
throw error
|
|
678
796
|
}
|
|
679
|
-
session.
|
|
797
|
+
session.setFinalSettlement(.rollback(settlement))
|
|
680
798
|
throw TransactionRollbackError()
|
|
681
799
|
}
|
|
682
800
|
}
|
|
@@ -688,36 +806,31 @@ public class SynchroModuleImpl: NSObject {
|
|
|
688
806
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
689
807
|
reject: @escaping RCTPromiseRejectBlock
|
|
690
808
|
) {
|
|
691
|
-
|
|
692
|
-
guard acceptingTransactions, let client = client else {
|
|
693
|
-
sessionsLock.unlock()
|
|
694
|
-
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
|
|
809
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
698
810
|
let txID = UUID().uuidString
|
|
699
|
-
let session = TransactionSession(isWrite: isWrite
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
811
|
+
let session = TransactionSession(isWrite: isWrite, beginSettlement: settlement)
|
|
812
|
+
let admittedClient = runtime.withLock { state -> SynchroClient? in
|
|
813
|
+
guard state.acceptingTransactions, let client = state.client else { return nil }
|
|
814
|
+
state.sessions[txID] = session
|
|
815
|
+
return client
|
|
816
|
+
}
|
|
817
|
+
guard let client = admittedClient else {
|
|
818
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
819
|
+
return
|
|
704
820
|
}
|
|
705
|
-
sessions[txID] = session
|
|
706
|
-
sessionsLock.unlock()
|
|
707
821
|
|
|
822
|
+
let runtime = runtime
|
|
708
823
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
709
824
|
defer {
|
|
710
825
|
session.close()
|
|
711
|
-
|
|
712
|
-
self.sessions.removeValue(forKey: txID)
|
|
713
|
-
self.sessionsLock.unlock()
|
|
826
|
+
_ = runtime.withLock { $0.sessions.removeValue(forKey: txID) }
|
|
714
827
|
session.markFinished()
|
|
715
828
|
}
|
|
716
829
|
do {
|
|
717
830
|
let finalResult: Result<Void, Error>
|
|
718
831
|
if isWrite {
|
|
719
832
|
let body: (ApplicationTransaction) throws -> Void = { transaction in
|
|
720
|
-
try
|
|
833
|
+
try Self.runTransactionLoop(
|
|
721
834
|
session: session,
|
|
722
835
|
txID: txID,
|
|
723
836
|
query: { sql, params in
|
|
@@ -738,7 +851,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
738
851
|
}
|
|
739
852
|
} else {
|
|
740
853
|
try client.readTransaction { db in
|
|
741
|
-
try
|
|
854
|
+
try Self.runTransactionLoop(
|
|
742
855
|
session: session,
|
|
743
856
|
txID: txID,
|
|
744
857
|
query: { sql, params in
|
|
@@ -787,18 +900,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
787
900
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
788
901
|
reject: @escaping RCTPromiseRejectBlock
|
|
789
902
|
) {
|
|
903
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
790
904
|
guard let session = getSession(txID) else {
|
|
791
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
905
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
792
906
|
return
|
|
793
907
|
}
|
|
794
|
-
let
|
|
795
|
-
|
|
796
|
-
case .success(let rows): resolve(rows)
|
|
797
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
908
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
909
|
+
let op = TransactionOp.query(sql: sql, params: capturedParams, settlement: settlement)
|
|
800
910
|
guard session.enqueue(op) else {
|
|
801
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
911
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
802
912
|
return
|
|
803
913
|
}
|
|
804
914
|
}
|
|
@@ -811,18 +921,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
811
921
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
812
922
|
reject: @escaping RCTPromiseRejectBlock
|
|
813
923
|
) {
|
|
924
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
814
925
|
guard let session = getSession(txID) else {
|
|
815
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
926
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
816
927
|
return
|
|
817
928
|
}
|
|
818
|
-
let
|
|
819
|
-
|
|
820
|
-
case .success(let row): resolve(row ?? NSNull())
|
|
821
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
822
|
-
}
|
|
823
|
-
}
|
|
929
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
930
|
+
let op = TransactionOp.queryOne(sql: sql, params: capturedParams, settlement: settlement)
|
|
824
931
|
guard session.enqueue(op) else {
|
|
825
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
932
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
826
933
|
return
|
|
827
934
|
}
|
|
828
935
|
}
|
|
@@ -835,22 +942,19 @@ public class SynchroModuleImpl: NSObject {
|
|
|
835
942
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
836
943
|
reject: @escaping RCTPromiseRejectBlock
|
|
837
944
|
) {
|
|
945
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
838
946
|
guard let session = getSession(txID) else {
|
|
839
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
947
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
840
948
|
return
|
|
841
949
|
}
|
|
842
950
|
guard session.isWrite else {
|
|
843
|
-
reject("DATABASE_ERROR", "Read transactions cannot execute SQL", nil)
|
|
951
|
+
settlement.reject("DATABASE_ERROR", "Read transactions cannot execute SQL", nil)
|
|
844
952
|
return
|
|
845
953
|
}
|
|
846
|
-
let
|
|
847
|
-
|
|
848
|
-
case .success(let dict): resolve(dict)
|
|
849
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
850
|
-
}
|
|
851
|
-
}
|
|
954
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
955
|
+
let op = TransactionOp.execute(sql: sql, params: capturedParams, settlement: settlement)
|
|
852
956
|
guard session.enqueue(op) else {
|
|
853
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
957
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
854
958
|
return
|
|
855
959
|
}
|
|
856
960
|
}
|
|
@@ -861,19 +965,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
861
965
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
862
966
|
reject: @escaping RCTPromiseRejectBlock
|
|
863
967
|
) {
|
|
968
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
864
969
|
guard let session = getSession(txID) else {
|
|
865
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
970
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
866
971
|
return
|
|
867
972
|
}
|
|
868
|
-
let op = TransactionOp.commit
|
|
869
|
-
switch result {
|
|
870
|
-
case .success: resolve(nil)
|
|
871
|
-
case .failure(let error as SynchroError): self.rejectWithError(reject, error)
|
|
872
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
873
|
-
}
|
|
874
|
-
}
|
|
973
|
+
let op = TransactionOp.commit(settlement: settlement)
|
|
875
974
|
guard session.enqueue(op) else {
|
|
876
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
975
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
877
976
|
return
|
|
878
977
|
}
|
|
879
978
|
}
|
|
@@ -884,79 +983,75 @@ public class SynchroModuleImpl: NSObject {
|
|
|
884
983
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
885
984
|
reject: @escaping RCTPromiseRejectBlock
|
|
886
985
|
) {
|
|
986
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
887
987
|
guard let session = getSession(txID) else {
|
|
888
|
-
resolve(
|
|
988
|
+
settlement.resolve(.void)
|
|
889
989
|
return
|
|
890
990
|
}
|
|
891
|
-
let op = TransactionOp.rollback
|
|
892
|
-
switch result {
|
|
893
|
-
case .success: resolve(nil)
|
|
894
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
895
|
-
}
|
|
896
|
-
}
|
|
991
|
+
let op = TransactionOp.rollback(settlement: settlement)
|
|
897
992
|
guard session.enqueue(op) else {
|
|
898
|
-
resolve(
|
|
993
|
+
settlement.resolve(.void)
|
|
899
994
|
return
|
|
900
995
|
}
|
|
901
996
|
}
|
|
902
997
|
|
|
903
998
|
private func getSession(_ txID: String) -> TransactionSession? {
|
|
904
|
-
|
|
905
|
-
defer { sessionsLock.unlock() }
|
|
906
|
-
return sessions[txID]
|
|
999
|
+
runtime.withLock { $0.sessions[txID] }
|
|
907
1000
|
}
|
|
908
1001
|
|
|
909
|
-
private func wireClientEvents(
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1002
|
+
private static func wireClientEvents(
|
|
1003
|
+
_ client: SynchroClient,
|
|
1004
|
+
runtime: OSAllocatedUnfairLock<RuntimeState>,
|
|
1005
|
+
eventTarget: ReactEventTarget
|
|
1006
|
+
) {
|
|
1007
|
+
runtime.withLock {
|
|
1008
|
+
$0.cachedBackoff = nil
|
|
1009
|
+
$0.cachedFailure = nil
|
|
1010
|
+
}
|
|
914
1011
|
|
|
915
|
-
let statusSubscription = client.onStatusChange {
|
|
1012
|
+
let statusSubscription = client.onStatusChange { status in
|
|
916
1013
|
guard status != .backoff, status != .error else { return }
|
|
917
|
-
|
|
1014
|
+
eventTarget.emit(
|
|
1015
|
+
"onStatusChange",
|
|
1016
|
+
body: bridgeValue(statusPayload(status, client: client, runtime: runtime))
|
|
1017
|
+
)
|
|
918
1018
|
}
|
|
919
1019
|
|
|
920
|
-
let syncEventSubscription = client.onSyncEvent {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
self.emit("onSyncEvent", self.syncEventPayload(event))
|
|
1020
|
+
let syncEventSubscription = client.onSyncEvent { event in
|
|
1021
|
+
recordStatusDetails(event, runtime: runtime)
|
|
1022
|
+
eventTarget.emit("onSyncEvent", body: bridgeValue(syncEventPayload(event)))
|
|
924
1023
|
switch event {
|
|
925
1024
|
case .backoff, .failure:
|
|
926
|
-
|
|
1025
|
+
eventTarget.emit(
|
|
927
1026
|
"onStatusChange",
|
|
928
|
-
|
|
1027
|
+
body: bridgeValue(statusPayload(client.getSyncStatus(), client: client, runtime: runtime))
|
|
929
1028
|
)
|
|
930
1029
|
default:
|
|
931
1030
|
break
|
|
932
1031
|
}
|
|
933
1032
|
}
|
|
934
1033
|
|
|
935
|
-
let conflictSubscription = client.onConflict {
|
|
936
|
-
|
|
1034
|
+
let conflictSubscription = client.onConflict { event in
|
|
1035
|
+
eventTarget.emit("onConflict", body: bridgeValue(conflictPayload(event)))
|
|
937
1036
|
}
|
|
938
1037
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
private func setClient(_ client: SynchroClient?, acceptingTransactions: Bool) {
|
|
947
|
-
sessionsLock.lock()
|
|
948
|
-
self.client = client
|
|
949
|
-
self.acceptingTransactions = acceptingTransactions
|
|
950
|
-
sessionsLock.unlock()
|
|
1038
|
+
runtime.withLock {
|
|
1039
|
+
$0.statusSubscription = statusSubscription
|
|
1040
|
+
$0.syncEventSubscription = syncEventSubscription
|
|
1041
|
+
$0.conflictSubscription = conflictSubscription
|
|
1042
|
+
}
|
|
951
1043
|
}
|
|
952
1044
|
|
|
953
|
-
private func withLifecycleLock(
|
|
954
|
-
|
|
1045
|
+
private static func withLifecycleLock(
|
|
1046
|
+
_ mutex: LifecycleMutex,
|
|
1047
|
+
operation: @escaping @Sendable () async throws -> Void
|
|
1048
|
+
) async throws {
|
|
1049
|
+
await mutex.lock()
|
|
955
1050
|
do {
|
|
956
1051
|
try await operation()
|
|
957
|
-
await
|
|
1052
|
+
await mutex.unlock()
|
|
958
1053
|
} catch {
|
|
959
|
-
await
|
|
1054
|
+
await mutex.unlock()
|
|
960
1055
|
throw error
|
|
961
1056
|
}
|
|
962
1057
|
}
|
|
@@ -964,74 +1059,71 @@ public class SynchroModuleImpl: NSObject {
|
|
|
964
1059
|
private func submitDatabaseOperation(
|
|
965
1060
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
966
1061
|
reject: @escaping RCTPromiseRejectBlock,
|
|
967
|
-
operation: @escaping (SynchroClient) throws ->
|
|
1062
|
+
operation: @escaping @Sendable (SynchroClient) throws -> BridgePromiseValue
|
|
968
1063
|
) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
self.rejectWithError(reject, error)
|
|
1064
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1065
|
+
let queue = databaseBridgeQueue
|
|
1066
|
+
let admissionGate = DispatchSemaphore(value: 0)
|
|
1067
|
+
let admitted = runtime.withLock { state -> Bool in
|
|
1068
|
+
guard state.acceptingTransactions, let client = state.client else { return false }
|
|
1069
|
+
queue.async {
|
|
1070
|
+
admissionGate.wait()
|
|
1071
|
+
do {
|
|
1072
|
+
settlement.resolve(try operation(client))
|
|
1073
|
+
} catch {
|
|
1074
|
+
Self.rejectWithError(settlement, error)
|
|
1075
|
+
}
|
|
982
1076
|
}
|
|
1077
|
+
return true
|
|
983
1078
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
transportObservations?.cancelPauseBarrier()
|
|
989
|
-
transportObservations = nil
|
|
990
|
-
statusDetailsLock.lock()
|
|
991
|
-
cachedBackoff = nil
|
|
992
|
-
cachedFailure = nil
|
|
993
|
-
statusDetailsLock.unlock()
|
|
994
|
-
|
|
995
|
-
sessionsLock.lock()
|
|
996
|
-
acceptingTransactions = false
|
|
997
|
-
let activeSessions = Array(sessions.values)
|
|
998
|
-
sessions.removeAll()
|
|
999
|
-
let activeObservers = Array(observers.values)
|
|
1000
|
-
observers.removeAll()
|
|
1001
|
-
let activeSubscriptions = (
|
|
1002
|
-
statusSubscription,
|
|
1003
|
-
syncEventSubscription,
|
|
1004
|
-
conflictSubscription
|
|
1005
|
-
)
|
|
1006
|
-
statusSubscription = nil
|
|
1007
|
-
syncEventSubscription = nil
|
|
1008
|
-
conflictSubscription = nil
|
|
1009
|
-
sessionsLock.unlock()
|
|
1010
|
-
activeObservers.forEach { $0.cancel() }
|
|
1011
|
-
activeSubscriptions.0?.cancel()
|
|
1012
|
-
activeSubscriptions.1?.cancel()
|
|
1013
|
-
activeSubscriptions.2?.cancel()
|
|
1014
|
-
|
|
1015
|
-
authLock.lock()
|
|
1016
|
-
let authContinuations = pendingAuthContinuations
|
|
1017
|
-
pendingAuthContinuations.removeAll()
|
|
1018
|
-
authLock.unlock()
|
|
1019
|
-
authContinuations.values.forEach { continuation in
|
|
1020
|
-
continuation.resume(throwing: NSError(
|
|
1021
|
-
domain: "SynchroModule",
|
|
1022
|
-
code: 0,
|
|
1023
|
-
userInfo: [NSLocalizedDescriptionKey: "client closed"]
|
|
1024
|
-
))
|
|
1079
|
+
admissionGate.signal()
|
|
1080
|
+
guard admitted else {
|
|
1081
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1082
|
+
return
|
|
1025
1083
|
}
|
|
1026
|
-
return activeSessions
|
|
1027
1084
|
}
|
|
1028
1085
|
|
|
1029
|
-
private func
|
|
1030
|
-
|
|
1031
|
-
|
|
1086
|
+
private static func detachRuntimeState(
|
|
1087
|
+
_ runtime: OSAllocatedUnfairLock<RuntimeState>
|
|
1088
|
+
) -> DetachedState {
|
|
1089
|
+
runtime.withLock { state in
|
|
1090
|
+
state.acceptingTransactions = false
|
|
1091
|
+
let detached = DetachedState(
|
|
1092
|
+
sessions: Array(state.sessions.values),
|
|
1093
|
+
observers: Array(state.observers.values),
|
|
1094
|
+
subscriptions: [
|
|
1095
|
+
state.statusSubscription,
|
|
1096
|
+
state.syncEventSubscription,
|
|
1097
|
+
state.conflictSubscription,
|
|
1098
|
+
].compactMap { $0 },
|
|
1099
|
+
transportObservations: state.transportObservations
|
|
1100
|
+
)
|
|
1101
|
+
state.sessions.removeAll()
|
|
1102
|
+
state.observers.removeAll()
|
|
1103
|
+
state.statusSubscription = nil
|
|
1104
|
+
state.syncEventSubscription = nil
|
|
1105
|
+
state.conflictSubscription = nil
|
|
1106
|
+
state.cachedBackoff = nil
|
|
1107
|
+
state.cachedFailure = nil
|
|
1108
|
+
state.transportObservations = nil
|
|
1109
|
+
return detached
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private static func clearRuntimeState(
|
|
1114
|
+
runtime: OSAllocatedUnfairLock<RuntimeState>,
|
|
1115
|
+
authRequests: AuthRequestStore,
|
|
1116
|
+
databaseBridgeQueue: DispatchQueue
|
|
1117
|
+
) async {
|
|
1118
|
+
let detached = detachRuntimeState(runtime)
|
|
1119
|
+
detached.transportObservations?.cancelPauseBarrier()
|
|
1120
|
+
detached.observers.forEach { $0.cancel() }
|
|
1121
|
+
detached.subscriptions.forEach { $0.cancel() }
|
|
1122
|
+
authRequests.cancelAll()
|
|
1123
|
+
detached.sessions.forEach {
|
|
1032
1124
|
$0.abort(TransactionAbortedError(message: "Client closed during transaction"))
|
|
1033
1125
|
}
|
|
1034
|
-
for session in
|
|
1126
|
+
for session in detached.sessions {
|
|
1035
1127
|
await session.waitUntilFinished()
|
|
1036
1128
|
}
|
|
1037
1129
|
await withCheckedContinuation { continuation in
|
|
@@ -1041,7 +1133,11 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1041
1133
|
}
|
|
1042
1134
|
}
|
|
1043
1135
|
|
|
1044
|
-
private func statusPayload(
|
|
1136
|
+
private static func statusPayload(
|
|
1137
|
+
_ status: SyncStatus,
|
|
1138
|
+
client: SynchroClient,
|
|
1139
|
+
runtime: OSAllocatedUnfairLock<RuntimeState>
|
|
1140
|
+
) -> [String: Any] {
|
|
1045
1141
|
var payload: [String: Any] = [
|
|
1046
1142
|
"status": status.rawValue,
|
|
1047
1143
|
"retryAt": NSNull(),
|
|
@@ -1052,17 +1148,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1052
1148
|
case .uninitialized, .localReady, .connecting, .schemaApplying, .ready, .pushing, .pulling, .rebuilding, .stopped:
|
|
1053
1149
|
break
|
|
1054
1150
|
case .backoff:
|
|
1055
|
-
|
|
1056
|
-
let backoff = cachedBackoff
|
|
1057
|
-
statusDetailsLock.unlock()
|
|
1151
|
+
let backoff = runtime.withLock { $0.cachedBackoff }
|
|
1058
1152
|
if let backoff {
|
|
1059
1153
|
payload["retryAt"] = iso8601String(backoff.retryAt)
|
|
1060
1154
|
payload["operation"] = backoff.operation.rawValue
|
|
1061
1155
|
}
|
|
1062
1156
|
case .error:
|
|
1063
|
-
|
|
1064
|
-
let cachedFailure = self.cachedFailure
|
|
1065
|
-
statusDetailsLock.unlock()
|
|
1157
|
+
let cachedFailure = runtime.withLock { $0.cachedFailure }
|
|
1066
1158
|
let persistedFailure: SyncFailure?
|
|
1067
1159
|
persistedFailure = try? client.getBlockingFailure()
|
|
1068
1160
|
if let failure = cachedFailure ?? persistedFailure {
|
|
@@ -1072,26 +1164,29 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1072
1164
|
return payload
|
|
1073
1165
|
}
|
|
1074
1166
|
|
|
1075
|
-
private func recordStatusDetails(
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1167
|
+
private static func recordStatusDetails(
|
|
1168
|
+
_ event: SyncEvent,
|
|
1169
|
+
runtime: OSAllocatedUnfairLock<RuntimeState>
|
|
1170
|
+
) {
|
|
1171
|
+
runtime.withLock { state in
|
|
1172
|
+
switch event {
|
|
1173
|
+
case .backoff(let backoff):
|
|
1174
|
+
state.cachedBackoff = backoff
|
|
1175
|
+
state.cachedFailure = nil
|
|
1176
|
+
case .failure(let failure):
|
|
1177
|
+
state.cachedFailure = failure
|
|
1178
|
+
state.cachedBackoff = nil
|
|
1179
|
+
default:
|
|
1180
|
+
break
|
|
1181
|
+
}
|
|
1087
1182
|
}
|
|
1088
1183
|
}
|
|
1089
1184
|
|
|
1090
|
-
private func schemaPayload(_ schema: SchemaRef) -> [String: Any] {
|
|
1185
|
+
private static func schemaPayload(_ schema: SchemaRef) -> [String: Any] {
|
|
1091
1186
|
["version": schema.version, "hash": schema.hash]
|
|
1092
1187
|
}
|
|
1093
1188
|
|
|
1094
|
-
private func failurePayload(_ failure: SyncFailure) -> [String: Any] {
|
|
1189
|
+
private static func failurePayload(_ failure: SyncFailure) -> [String: Any] {
|
|
1095
1190
|
[
|
|
1096
1191
|
"operation": failure.operation.rawValue,
|
|
1097
1192
|
"code": failure.code.rawValue,
|
|
@@ -1102,13 +1197,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1102
1197
|
]
|
|
1103
1198
|
}
|
|
1104
1199
|
|
|
1105
|
-
private func iso8601String(_ date: Date) -> String {
|
|
1200
|
+
private static func iso8601String(_ date: Date) -> String {
|
|
1106
1201
|
let formatter = ISO8601DateFormatter()
|
|
1107
1202
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
1108
1203
|
return formatter.string(from: date)
|
|
1109
1204
|
}
|
|
1110
1205
|
|
|
1111
|
-
private func syncEventPayload(_ event: SyncEvent) -> [String: Any] {
|
|
1206
|
+
private static func syncEventPayload(_ event: SyncEvent) -> [String: Any] {
|
|
1112
1207
|
var payload: [String: Any] = [
|
|
1113
1208
|
"type": "",
|
|
1114
1209
|
"from": NSNull(),
|
|
@@ -1175,7 +1270,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1175
1270
|
return payload
|
|
1176
1271
|
}
|
|
1177
1272
|
|
|
1178
|
-
private func conflictPayload(_ event: ConflictEvent) -> [String: Any] {
|
|
1273
|
+
private static func conflictPayload(_ event: ConflictEvent) -> [String: Any] {
|
|
1179
1274
|
[
|
|
1180
1275
|
"table": event.table,
|
|
1181
1276
|
"recordID": event.recordID,
|
|
@@ -1184,7 +1279,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1184
1279
|
]
|
|
1185
1280
|
}
|
|
1186
1281
|
|
|
1187
|
-
private func encodeRejectedMutations(_ results: [RejectedMutation]) -> String {
|
|
1282
|
+
private static func encodeRejectedMutations(_ results: [RejectedMutation]) -> String {
|
|
1188
1283
|
let payload = results.map { result in
|
|
1189
1284
|
[
|
|
1190
1285
|
"mutationID": result.mutationID,
|
|
@@ -1207,12 +1302,12 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1207
1302
|
return json
|
|
1208
1303
|
}
|
|
1209
1304
|
|
|
1210
|
-
private func anyCodableMapToJSONObject(_ value: [String: AnyCodable]?) -> Any {
|
|
1305
|
+
private static func anyCodableMapToJSONObject(_ value: [String: AnyCodable]?) -> Any {
|
|
1211
1306
|
guard let value else { return NSNull() }
|
|
1212
1307
|
return value.mapValues { anyCodableToJSONObject($0.value) }
|
|
1213
1308
|
}
|
|
1214
1309
|
|
|
1215
|
-
private func anyCodableToJSONObject(_ value: Any?) -> Any {
|
|
1310
|
+
private static func anyCodableToJSONObject(_ value: Any?) -> Any {
|
|
1216
1311
|
switch value {
|
|
1217
1312
|
case nil:
|
|
1218
1313
|
return NSNull()
|
|
@@ -1239,7 +1334,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1239
1334
|
}
|
|
1240
1335
|
}
|
|
1241
1336
|
|
|
1242
|
-
private func encodeAnyCodableMap(_ value: [String: AnyCodable]?) -> String? {
|
|
1337
|
+
private static func encodeAnyCodableMap(_ value: [String: AnyCodable]?) -> String? {
|
|
1243
1338
|
guard let value else { return nil }
|
|
1244
1339
|
let encoder = JSONEncoder()
|
|
1245
1340
|
guard let data = try? encoder.encode(value) else { return nil }
|
|
@@ -1260,10 +1355,10 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1260
1355
|
resolve: resolve,
|
|
1261
1356
|
reject: reject,
|
|
1262
1357
|
operation: { client in
|
|
1263
|
-
let columns = try
|
|
1264
|
-
let options = try optionsJson.flatMap { try
|
|
1358
|
+
let columns = try Self.parseColumns(columnsJson)
|
|
1359
|
+
let options = try optionsJson.flatMap { try Self.parseTableOptions($0) }
|
|
1265
1360
|
try client.createTable(name, columns: columns, options: options)
|
|
1266
|
-
return
|
|
1361
|
+
return .void
|
|
1267
1362
|
}
|
|
1268
1363
|
)
|
|
1269
1364
|
}
|
|
@@ -1279,9 +1374,9 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1279
1374
|
resolve: resolve,
|
|
1280
1375
|
reject: reject,
|
|
1281
1376
|
operation: { client in
|
|
1282
|
-
let columns = try
|
|
1377
|
+
let columns = try Self.parseColumns(columnsJson)
|
|
1283
1378
|
try client.alterTable(name, addColumns: columns)
|
|
1284
|
-
return
|
|
1379
|
+
return .void
|
|
1285
1380
|
}
|
|
1286
1381
|
)
|
|
1287
1382
|
}
|
|
@@ -1299,7 +1394,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1299
1394
|
reject: reject,
|
|
1300
1395
|
operation: { client in
|
|
1301
1396
|
try client.createIndex(table, columns: columns, unique: unique)
|
|
1302
|
-
return
|
|
1397
|
+
return .void
|
|
1303
1398
|
}
|
|
1304
1399
|
)
|
|
1305
1400
|
}
|
|
@@ -1313,18 +1408,29 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1313
1408
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1314
1409
|
reject: @escaping RCTPromiseRejectBlock
|
|
1315
1410
|
) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1411
|
+
guard let client = runtime.withLock({ state in
|
|
1412
|
+
state.acceptingTransactions ? state.client : nil
|
|
1413
|
+
}) else {
|
|
1319
1414
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1320
1415
|
return
|
|
1321
1416
|
}
|
|
1322
|
-
let
|
|
1323
|
-
|
|
1417
|
+
let eventTarget = eventTarget
|
|
1418
|
+
let cancellable = client.onChange(tables: tables) {
|
|
1419
|
+
eventTarget.emit(
|
|
1420
|
+
"onChange",
|
|
1421
|
+
body: .object(["observerID": .string(observerID)])
|
|
1422
|
+
)
|
|
1423
|
+
}
|
|
1424
|
+
let registration = runtime.withLock { state -> (Bool, (any Synchro.Cancellable)?) in
|
|
1425
|
+
guard state.acceptingTransactions, state.client === client else { return (false, nil) }
|
|
1426
|
+
return (true, state.observers.updateValue(cancellable, forKey: observerID))
|
|
1427
|
+
}
|
|
1428
|
+
guard registration.0 else {
|
|
1429
|
+
cancellable.cancel()
|
|
1430
|
+
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1431
|
+
return
|
|
1324
1432
|
}
|
|
1325
|
-
|
|
1326
|
-
sessionsLock.unlock()
|
|
1327
|
-
previous?.cancel()
|
|
1433
|
+
registration.1?.cancel()
|
|
1328
1434
|
resolve(nil)
|
|
1329
1435
|
}
|
|
1330
1436
|
|
|
@@ -1337,25 +1443,36 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1337
1443
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1338
1444
|
reject: @escaping RCTPromiseRejectBlock
|
|
1339
1445
|
) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1446
|
+
guard let client = runtime.withLock({ state in
|
|
1447
|
+
state.acceptingTransactions ? state.client : nil
|
|
1448
|
+
}) else {
|
|
1343
1449
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1344
1450
|
return
|
|
1345
1451
|
}
|
|
1346
1452
|
do {
|
|
1347
|
-
let nativeParams = try bridgeParams(params)
|
|
1348
|
-
let
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1453
|
+
let nativeParams = try Self.bridgeParams(Self.captureSQLParams(params))
|
|
1454
|
+
let eventTarget = eventTarget
|
|
1455
|
+
let cancellable = client.watch(sql, params: nativeParams, tables: tables) { rows in
|
|
1456
|
+
eventTarget.emit(
|
|
1457
|
+
"onQueryResult",
|
|
1458
|
+
body: .object([
|
|
1459
|
+
"observerID": .string(observerID),
|
|
1460
|
+
"rows": Self.rowsToBridgeValue(rows),
|
|
1461
|
+
])
|
|
1462
|
+
)
|
|
1463
|
+
}
|
|
1464
|
+
let registration = runtime.withLock { state -> (Bool, (any Synchro.Cancellable)?) in
|
|
1465
|
+
guard state.acceptingTransactions, state.client === client else { return (false, nil) }
|
|
1466
|
+
return (true, state.observers.updateValue(cancellable, forKey: observerID))
|
|
1352
1467
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1468
|
+
guard registration.0 else {
|
|
1469
|
+
cancellable.cancel()
|
|
1470
|
+
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1471
|
+
return
|
|
1472
|
+
}
|
|
1473
|
+
registration.1?.cancel()
|
|
1356
1474
|
resolve(nil)
|
|
1357
1475
|
} catch {
|
|
1358
|
-
sessionsLock.unlock()
|
|
1359
1476
|
rejectWithError(reject, error)
|
|
1360
1477
|
}
|
|
1361
1478
|
}
|
|
@@ -1366,9 +1483,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1366
1483
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1367
1484
|
reject: @escaping RCTPromiseRejectBlock
|
|
1368
1485
|
) {
|
|
1369
|
-
|
|
1370
|
-
let cancellable = observers.removeValue(forKey: observerID)
|
|
1371
|
-
sessionsLock.unlock()
|
|
1486
|
+
let cancellable = runtime.withLock { $0.observers.removeValue(forKey: observerID) }
|
|
1372
1487
|
cancellable?.cancel()
|
|
1373
1488
|
resolve(nil)
|
|
1374
1489
|
}
|
|
@@ -1378,19 +1493,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1378
1493
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1379
1494
|
reject: @escaping RCTPromiseRejectBlock
|
|
1380
1495
|
) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1496
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1497
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1498
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1383
1499
|
return
|
|
1384
1500
|
}
|
|
1385
1501
|
Task {
|
|
1386
1502
|
do {
|
|
1387
1503
|
try await client.start()
|
|
1388
1504
|
DispatchQueue.main.async {
|
|
1389
|
-
resolve(
|
|
1505
|
+
settlement.resolve(.void)
|
|
1390
1506
|
}
|
|
1391
1507
|
} catch {
|
|
1392
1508
|
DispatchQueue.main.async {
|
|
1393
|
-
|
|
1509
|
+
Self.rejectWithError(settlement, error)
|
|
1394
1510
|
}
|
|
1395
1511
|
}
|
|
1396
1512
|
}
|
|
@@ -1401,14 +1517,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1401
1517
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1402
1518
|
reject: @escaping RCTPromiseRejectBlock
|
|
1403
1519
|
) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1520
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1521
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1522
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1406
1523
|
return
|
|
1407
1524
|
}
|
|
1408
1525
|
Task {
|
|
1409
1526
|
await client.stop()
|
|
1410
1527
|
DispatchQueue.main.async {
|
|
1411
|
-
resolve(
|
|
1528
|
+
settlement.resolve(.void)
|
|
1412
1529
|
}
|
|
1413
1530
|
}
|
|
1414
1531
|
}
|
|
@@ -1418,14 +1535,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1418
1535
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1419
1536
|
reject: @escaping RCTPromiseRejectBlock
|
|
1420
1537
|
) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1538
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1539
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1540
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1423
1541
|
return
|
|
1424
1542
|
}
|
|
1425
1543
|
Task {
|
|
1426
1544
|
await client.enterBackground()
|
|
1427
1545
|
DispatchQueue.main.async {
|
|
1428
|
-
resolve(
|
|
1546
|
+
settlement.resolve(.void)
|
|
1429
1547
|
}
|
|
1430
1548
|
}
|
|
1431
1549
|
}
|
|
@@ -1435,19 +1553,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1435
1553
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1436
1554
|
reject: @escaping RCTPromiseRejectBlock
|
|
1437
1555
|
) {
|
|
1438
|
-
|
|
1439
|
-
|
|
1556
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1557
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1558
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1440
1559
|
return
|
|
1441
1560
|
}
|
|
1442
1561
|
Task {
|
|
1443
1562
|
do {
|
|
1444
1563
|
try await client.enterForeground()
|
|
1445
1564
|
DispatchQueue.main.async {
|
|
1446
|
-
resolve(
|
|
1565
|
+
settlement.resolve(.void)
|
|
1447
1566
|
}
|
|
1448
1567
|
} catch {
|
|
1449
1568
|
DispatchQueue.main.async {
|
|
1450
|
-
|
|
1569
|
+
Self.rejectWithError(settlement, error)
|
|
1451
1570
|
}
|
|
1452
1571
|
}
|
|
1453
1572
|
}
|
|
@@ -1458,19 +1577,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1458
1577
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1459
1578
|
reject: @escaping RCTPromiseRejectBlock
|
|
1460
1579
|
) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1580
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1581
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1582
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1463
1583
|
return
|
|
1464
1584
|
}
|
|
1465
1585
|
Task {
|
|
1466
1586
|
do {
|
|
1467
1587
|
try await client.retryAfterError()
|
|
1468
1588
|
DispatchQueue.main.async {
|
|
1469
|
-
resolve(
|
|
1589
|
+
settlement.resolve(.void)
|
|
1470
1590
|
}
|
|
1471
1591
|
} catch {
|
|
1472
1592
|
DispatchQueue.main.async {
|
|
1473
|
-
|
|
1593
|
+
Self.rejectWithError(settlement, error)
|
|
1474
1594
|
}
|
|
1475
1595
|
}
|
|
1476
1596
|
}
|
|
@@ -1481,19 +1601,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1481
1601
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1482
1602
|
reject: @escaping RCTPromiseRejectBlock
|
|
1483
1603
|
) {
|
|
1484
|
-
|
|
1485
|
-
|
|
1604
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1605
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1606
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1486
1607
|
return
|
|
1487
1608
|
}
|
|
1488
1609
|
Task {
|
|
1489
1610
|
do {
|
|
1490
1611
|
try await client.resetSchemaAndStart()
|
|
1491
1612
|
DispatchQueue.main.async {
|
|
1492
|
-
resolve(
|
|
1613
|
+
settlement.resolve(.void)
|
|
1493
1614
|
}
|
|
1494
1615
|
} catch {
|
|
1495
1616
|
DispatchQueue.main.async {
|
|
1496
|
-
|
|
1617
|
+
Self.rejectWithError(settlement, error)
|
|
1497
1618
|
}
|
|
1498
1619
|
}
|
|
1499
1620
|
}
|
|
@@ -1504,19 +1625,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1504
1625
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1505
1626
|
reject: @escaping RCTPromiseRejectBlock
|
|
1506
1627
|
) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1628
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1629
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
1630
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1509
1631
|
return
|
|
1510
1632
|
}
|
|
1511
1633
|
Task {
|
|
1512
1634
|
do {
|
|
1513
1635
|
try await client.syncNow()
|
|
1514
1636
|
DispatchQueue.main.async {
|
|
1515
|
-
resolve(
|
|
1637
|
+
settlement.resolve(.void)
|
|
1516
1638
|
}
|
|
1517
1639
|
} catch {
|
|
1518
1640
|
DispatchQueue.main.async {
|
|
1519
|
-
|
|
1641
|
+
Self.rejectWithError(settlement, error)
|
|
1520
1642
|
}
|
|
1521
1643
|
}
|
|
1522
1644
|
}
|
|
@@ -1530,7 +1652,9 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1530
1652
|
submitDatabaseOperation(
|
|
1531
1653
|
resolve: resolve,
|
|
1532
1654
|
reject: reject,
|
|
1533
|
-
operation: { client in
|
|
1655
|
+
operation: { client in
|
|
1656
|
+
.value(.integer(Int64(try client.pendingChangeCount())))
|
|
1657
|
+
}
|
|
1534
1658
|
)
|
|
1535
1659
|
}
|
|
1536
1660
|
|
|
@@ -1539,11 +1663,16 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1539
1663
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1540
1664
|
reject: @escaping RCTPromiseRejectBlock
|
|
1541
1665
|
) {
|
|
1666
|
+
let runtime = runtime
|
|
1542
1667
|
submitDatabaseOperation(
|
|
1543
1668
|
resolve: resolve,
|
|
1544
1669
|
reject: reject,
|
|
1545
1670
|
operation: { client in
|
|
1546
|
-
try
|
|
1671
|
+
.value(.string(try Self.encodeBridgeJSON(Self.statusPayload(
|
|
1672
|
+
client.getSyncStatus(),
|
|
1673
|
+
client: client,
|
|
1674
|
+
runtime: runtime
|
|
1675
|
+
))))
|
|
1547
1676
|
}
|
|
1548
1677
|
)
|
|
1549
1678
|
}
|
|
@@ -1557,8 +1686,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1557
1686
|
resolve: resolve,
|
|
1558
1687
|
reject: reject,
|
|
1559
1688
|
operation: { client in
|
|
1560
|
-
let payload = try client.inspectPendingMutations().map(
|
|
1561
|
-
return try
|
|
1689
|
+
let payload = try client.inspectPendingMutations().map(Self.pendingMutationPayload)
|
|
1690
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1562
1691
|
}
|
|
1563
1692
|
)
|
|
1564
1693
|
}
|
|
@@ -1572,8 +1701,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1572
1701
|
resolve: resolve,
|
|
1573
1702
|
reject: reject,
|
|
1574
1703
|
operation: { client in
|
|
1575
|
-
let payload = try client.inspectRetainedMutations().map(
|
|
1576
|
-
return try
|
|
1704
|
+
let payload = try client.inspectRetainedMutations().map(Self.pendingMutationPayload)
|
|
1705
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1577
1706
|
}
|
|
1578
1707
|
)
|
|
1579
1708
|
}
|
|
@@ -1587,8 +1716,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1587
1716
|
resolve: resolve,
|
|
1588
1717
|
reject: reject,
|
|
1589
1718
|
operation: { client in
|
|
1590
|
-
let payload = try client.inspectRetainedMutationRecords().map(
|
|
1591
|
-
return try
|
|
1719
|
+
let payload = try client.inspectRetainedMutationRecords().map(Self.retainedMutationPayload)
|
|
1720
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1592
1721
|
}
|
|
1593
1722
|
)
|
|
1594
1723
|
}
|
|
@@ -1602,8 +1731,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1602
1731
|
resolve: resolve,
|
|
1603
1732
|
reject: reject,
|
|
1604
1733
|
operation: { client in
|
|
1605
|
-
let payload = try client.inspectRejectedMutations().map(
|
|
1606
|
-
return try
|
|
1734
|
+
let payload = try client.inspectRejectedMutations().map(Self.rejectedMutationPayload)
|
|
1735
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1607
1736
|
}
|
|
1608
1737
|
)
|
|
1609
1738
|
}
|
|
@@ -1617,8 +1746,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1617
1746
|
resolve: resolve,
|
|
1618
1747
|
reject: reject,
|
|
1619
1748
|
operation: { client in
|
|
1620
|
-
let payload = try client.inspectRejectedMutationRecords().map(
|
|
1621
|
-
return try
|
|
1749
|
+
let payload = try client.inspectRejectedMutationRecords().map(Self.retainedRejectionPayload)
|
|
1750
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1622
1751
|
}
|
|
1623
1752
|
)
|
|
1624
1753
|
}
|
|
@@ -1631,11 +1760,12 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1631
1760
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1632
1761
|
reject: @escaping RCTPromiseRejectBlock
|
|
1633
1762
|
) {
|
|
1763
|
+
let capturedStatements = Result { try Self.captureBridgeStatements(rowStatements) }
|
|
1634
1764
|
submitDatabaseOperation(
|
|
1635
1765
|
resolve: resolve,
|
|
1636
1766
|
reject: reject,
|
|
1637
1767
|
operation: { client in
|
|
1638
|
-
let statements = try
|
|
1768
|
+
let statements = try Self.bridgeStatements(capturedStatements.get())
|
|
1639
1769
|
var rows: [Row] = []
|
|
1640
1770
|
let snapshot = try SynchroInspection(client: client).captureSnapshot(maximumRecords: 512) { _, transaction in
|
|
1641
1771
|
for statement in statements {
|
|
@@ -1643,7 +1773,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1643
1773
|
}
|
|
1644
1774
|
}
|
|
1645
1775
|
let inspection: [String: Any] = [
|
|
1646
|
-
"client_state":
|
|
1776
|
+
"client_state": Self.clientStatePayload(snapshot.capture),
|
|
1647
1777
|
"migration_journal": try snapshot.capture.migrationJournal.map {
|
|
1648
1778
|
try JSONSerialization.jsonObject(with: JSONEncoder().encode($0))
|
|
1649
1779
|
} ?? NSNull(),
|
|
@@ -1652,13 +1782,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1652
1782
|
"physical_schema_truncated": snapshot.capture.physicalSchemaTruncated,
|
|
1653
1783
|
"accepted_mutation_outcomes": snapshot.capture.acceptedMutationOutcomes,
|
|
1654
1784
|
"accepted_mutation_outcomes_truncated": snapshot.capture.acceptedMutationOutcomesTruncated,
|
|
1655
|
-
"retained_mutations": snapshot.retainedMutations.map { $0.map(
|
|
1656
|
-
"rejected_mutations": snapshot.rejectedMutations.map { $0.map(
|
|
1657
|
-
]
|
|
1658
|
-
return [
|
|
1659
|
-
"inspection": try self.encodeBridgeJSON(inspection),
|
|
1660
|
-
"applicationRows": self.rowsToBridgeRows(rows),
|
|
1785
|
+
"retained_mutations": snapshot.retainedMutations.map { $0.map(Self.retainedMutationPayload) } ?? NSNull(),
|
|
1786
|
+
"rejected_mutations": snapshot.rejectedMutations.map { $0.map(Self.retainedRejectionPayload) } ?? NSNull(),
|
|
1661
1787
|
]
|
|
1788
|
+
return .value(.object([
|
|
1789
|
+
"inspection": .string(try Self.encodeBridgeJSON(inspection)),
|
|
1790
|
+
"applicationRows": Self.rowsToBridgeValue(rows),
|
|
1791
|
+
]))
|
|
1662
1792
|
}
|
|
1663
1793
|
)
|
|
1664
1794
|
}
|
|
@@ -1704,10 +1834,10 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1704
1834
|
"local_scope_checksum": value.localScopeChecksum ?? NSNull(),
|
|
1705
1835
|
] as [String: Any]
|
|
1706
1836
|
}
|
|
1707
|
-
return try
|
|
1837
|
+
return .value(.string(try Self.encodeBridgeJSON([
|
|
1708
1838
|
"row_metadata": metadata,
|
|
1709
1839
|
"rebuild_receipts": receipts,
|
|
1710
|
-
])
|
|
1840
|
+
])))
|
|
1711
1841
|
}
|
|
1712
1842
|
)
|
|
1713
1843
|
}
|
|
@@ -1717,7 +1847,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1717
1847
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1718
1848
|
reject: @escaping RCTPromiseRejectBlock
|
|
1719
1849
|
) {
|
|
1720
|
-
guard let transportObservations else {
|
|
1850
|
+
guard let transportObservations = runtime.withLock({ $0.transportObservations }) else {
|
|
1721
1851
|
reject("NOT_CONNECTED", "Transport observation is not configured", nil)
|
|
1722
1852
|
return
|
|
1723
1853
|
}
|
|
@@ -1738,7 +1868,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1738
1868
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1739
1869
|
reject: @escaping RCTPromiseRejectBlock
|
|
1740
1870
|
) {
|
|
1741
|
-
guard let collector = transportObservations else {
|
|
1871
|
+
guard let collector = runtime.withLock({ $0.transportObservations }) else {
|
|
1742
1872
|
reject("INVALID_CONFIG", "Transport pause operation is invalid", nil)
|
|
1743
1873
|
return
|
|
1744
1874
|
}
|
|
@@ -1763,9 +1893,10 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1763
1893
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1764
1894
|
reject: @escaping RCTPromiseRejectBlock
|
|
1765
1895
|
) {
|
|
1766
|
-
|
|
1896
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1897
|
+
guard let collector = runtime.withLock({ $0.transportObservations }),
|
|
1767
1898
|
timeoutMs.isFinite, timeoutMs >= 1, timeoutMs <= 60_000 else {
|
|
1768
|
-
reject("INVALID_CONFIG", "Transport pause wait is invalid", nil)
|
|
1899
|
+
settlement.reject("INVALID_CONFIG", "Transport pause wait is invalid", nil)
|
|
1769
1900
|
return
|
|
1770
1901
|
}
|
|
1771
1902
|
Task {
|
|
@@ -1777,9 +1908,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1777
1908
|
} else {
|
|
1778
1909
|
throw SynchroError.invalidResponse(message: "pause target is invalid")
|
|
1779
1910
|
}
|
|
1780
|
-
DispatchQueue.main.async {
|
|
1911
|
+
DispatchQueue.main.async {
|
|
1912
|
+
settlement.resolve(.void)
|
|
1913
|
+
}
|
|
1781
1914
|
} catch {
|
|
1782
|
-
DispatchQueue.main.async {
|
|
1915
|
+
DispatchQueue.main.async {
|
|
1916
|
+
Self.rejectWithError(settlement, error)
|
|
1917
|
+
}
|
|
1783
1918
|
}
|
|
1784
1919
|
}
|
|
1785
1920
|
}
|
|
@@ -1789,7 +1924,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1789
1924
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1790
1925
|
reject: @escaping RCTPromiseRejectBlock
|
|
1791
1926
|
) {
|
|
1792
|
-
guard let collector = transportObservations else {
|
|
1927
|
+
guard let collector = runtime.withLock({ $0.transportObservations }) else {
|
|
1793
1928
|
reject("NOT_CONNECTED", "Transport observation is not configured", nil)
|
|
1794
1929
|
return
|
|
1795
1930
|
}
|
|
@@ -1819,14 +1954,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1819
1954
|
reject: reject,
|
|
1820
1955
|
operation: { client in
|
|
1821
1956
|
try client.clearRejectedMutations()
|
|
1822
|
-
return
|
|
1957
|
+
return .void
|
|
1823
1958
|
}
|
|
1824
1959
|
)
|
|
1825
1960
|
}
|
|
1826
1961
|
|
|
1827
1962
|
// MARK: - Helpers
|
|
1828
1963
|
|
|
1829
|
-
private func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
|
|
1964
|
+
private static func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
|
|
1830
1965
|
let schema: Any = capture.schema.map { value in
|
|
1831
1966
|
["version": value.version, "hash": value.hash]
|
|
1832
1967
|
} ?? NSNull()
|
|
@@ -1888,17 +2023,119 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1888
2023
|
]
|
|
1889
2024
|
}
|
|
1890
2025
|
|
|
1891
|
-
private
|
|
2026
|
+
private struct CapturedBridgeStatement: Sendable {
|
|
2027
|
+
let sql: String
|
|
2028
|
+
let params: [BridgeValue]?
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
private static func captureSQLParams<Values: Sequence>(
|
|
2032
|
+
_ values: Values
|
|
2033
|
+
) throws -> [BridgeValue] where Values.Element == Any {
|
|
2034
|
+
try values.enumerated().map { index, value in
|
|
2035
|
+
try captureSQLParam(value, index: index)
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private static func captureSQLParam(_ value: Any, index: Int) throws -> BridgeValue {
|
|
2040
|
+
if let scalar = try captureBridgeScalar(value, index: index) {
|
|
2041
|
+
return scalar
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
switch value {
|
|
2045
|
+
case let value as NSDictionary:
|
|
2046
|
+
let validated = try bridgeTagToParam(
|
|
2047
|
+
fieldCount: value.count,
|
|
2048
|
+
stringField: { value[$0] as? String },
|
|
2049
|
+
containsField: { value[$0] != nil },
|
|
2050
|
+
index: index
|
|
2051
|
+
)
|
|
2052
|
+
return .object(validated.tag)
|
|
2053
|
+
case is NSArray:
|
|
2054
|
+
throw BridgeParamError.unsupported(index: index, type: "array")
|
|
2055
|
+
default:
|
|
2056
|
+
throw BridgeParamError.unsupported(index: index, type: String(describing: type(of: value)))
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
private static func captureBridgeScalar(_ value: Any, index: Int) throws -> BridgeValue? {
|
|
2061
|
+
switch value {
|
|
2062
|
+
case is NSNull:
|
|
2063
|
+
return .null
|
|
2064
|
+
case let value as String:
|
|
2065
|
+
return .string(value)
|
|
2066
|
+
case let value as NSNumber:
|
|
2067
|
+
if CFGetTypeID(value) == CFBooleanGetTypeID() {
|
|
2068
|
+
return .bool(value.boolValue)
|
|
2069
|
+
}
|
|
2070
|
+
let doubleValue = value.doubleValue
|
|
2071
|
+
guard doubleValue.isFinite else {
|
|
2072
|
+
throw BridgeParamError.invalidNumber(index: index)
|
|
2073
|
+
}
|
|
2074
|
+
if doubleValue.rounded(.towardZero) == doubleValue,
|
|
2075
|
+
doubleValue >= Double(Int64.min),
|
|
2076
|
+
doubleValue < Double(Int64.max) {
|
|
2077
|
+
return .integer(Int64(doubleValue))
|
|
2078
|
+
}
|
|
2079
|
+
return .double(doubleValue)
|
|
2080
|
+
default:
|
|
2081
|
+
return nil
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
private static func captureBridgeValue(_ value: Any, index: Int) throws -> BridgeValue {
|
|
2086
|
+
if let scalar = try captureBridgeScalar(value, index: index) {
|
|
2087
|
+
return scalar
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
switch value {
|
|
2091
|
+
case let value as NSDictionary:
|
|
2092
|
+
var object: [String: BridgeValue] = [:]
|
|
2093
|
+
for (key, fieldValue) in value {
|
|
2094
|
+
guard let key = key as? String else {
|
|
2095
|
+
throw BridgeParamError.unsupported(index: index, type: "non-string object key")
|
|
2096
|
+
}
|
|
2097
|
+
object[key] = try captureBridgeValue(fieldValue, index: index)
|
|
2098
|
+
}
|
|
2099
|
+
return .object(object)
|
|
2100
|
+
case let value as NSArray:
|
|
2101
|
+
return .array(try value.map { try captureBridgeValue($0, index: index) })
|
|
2102
|
+
default:
|
|
2103
|
+
throw BridgeParamError.unsupported(index: index, type: String(describing: type(of: value)))
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
private static func bridgeValue(_ value: Any) -> BridgeValue {
|
|
2108
|
+
do {
|
|
2109
|
+
return try captureBridgeValue(value, index: 0)
|
|
2110
|
+
} catch {
|
|
2111
|
+
preconditionFailure("Internal bridge value is invalid: \(error.localizedDescription)")
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
private static func captureBridgeStatements(
|
|
2116
|
+
_ statements: [[String: Any]]
|
|
2117
|
+
) throws -> [CapturedBridgeStatement] {
|
|
1892
2118
|
try statements.enumerated().map { index, item in
|
|
1893
2119
|
guard let sql = item["sql"] as? String else {
|
|
1894
2120
|
throw SynchroError.invalidResponse(message: "Missing SQL at index \(index)")
|
|
1895
2121
|
}
|
|
1896
|
-
let params = try (item["params"] as?
|
|
1897
|
-
return
|
|
2122
|
+
let params = try (item["params"] as? NSArray).map(captureSQLParams)
|
|
2123
|
+
return CapturedBridgeStatement(sql: sql, params: params)
|
|
1898
2124
|
}
|
|
1899
2125
|
}
|
|
1900
2126
|
|
|
1901
|
-
private func
|
|
2127
|
+
private static func bridgeStatements(
|
|
2128
|
+
_ statements: [CapturedBridgeStatement]
|
|
2129
|
+
) throws -> [Synchro.SQLStatement] {
|
|
2130
|
+
try statements.map { statement in
|
|
2131
|
+
Synchro.SQLStatement(
|
|
2132
|
+
sql: statement.sql,
|
|
2133
|
+
params: try statement.params.map(bridgeParams)
|
|
2134
|
+
)
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
private static func encodeBridgeJSON(_ value: Any) throws -> String {
|
|
1902
2139
|
guard JSONSerialization.isValidJSONObject(value) else {
|
|
1903
2140
|
throw SynchroError.invalidResponse(message: "bridge JSON value is invalid")
|
|
1904
2141
|
}
|
|
@@ -1909,7 +2146,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1909
2146
|
return json
|
|
1910
2147
|
}
|
|
1911
2148
|
|
|
1912
|
-
private func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
|
|
2149
|
+
private static func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
|
|
1913
2150
|
switch value {
|
|
1914
2151
|
case .current(let mutation):
|
|
1915
2152
|
return pendingMutationPayload(mutation).merging(["representation": "current"]) { current, _ in current }
|
|
@@ -1929,7 +2166,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1929
2166
|
}
|
|
1930
2167
|
}
|
|
1931
2168
|
|
|
1932
|
-
private func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
|
|
2169
|
+
private static func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
|
|
1933
2170
|
[
|
|
1934
2171
|
"mutationID": mutation.mutationID,
|
|
1935
2172
|
"localOrder": mutation.localOrder,
|
|
@@ -1961,7 +2198,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1961
2198
|
]
|
|
1962
2199
|
}
|
|
1963
2200
|
|
|
1964
|
-
private func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
|
|
2201
|
+
private static func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
|
|
1965
2202
|
switch value {
|
|
1966
2203
|
case .current(let rejection):
|
|
1967
2204
|
return rejectedMutationPayload(rejection).merging(["representation": "current"]) { current, _ in current }
|
|
@@ -1982,7 +2219,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1982
2219
|
}
|
|
1983
2220
|
}
|
|
1984
2221
|
|
|
1985
|
-
private func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
|
|
2222
|
+
private static func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
|
|
1986
2223
|
[
|
|
1987
2224
|
"mutationID": mutation.mutationID,
|
|
1988
2225
|
"tableName": mutation.tableName,
|
|
@@ -2019,14 +2256,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2019
2256
|
private static let maxSafeInteger: Int64 = 9_007_199_254_740_991
|
|
2020
2257
|
private static let minSafeInteger: Int64 = -9_007_199_254_740_991
|
|
2021
2258
|
|
|
2022
|
-
private func canonicalBase64URL(_ data: Data) -> String {
|
|
2259
|
+
private static func canonicalBase64URL(_ data: Data) -> String {
|
|
2023
2260
|
data.base64EncodedString()
|
|
2024
2261
|
.replacingOccurrences(of: "+", with: "-")
|
|
2025
2262
|
.replacingOccurrences(of: "/", with: "_")
|
|
2026
2263
|
.replacingOccurrences(of: "=", with: "")
|
|
2027
2264
|
}
|
|
2028
2265
|
|
|
2029
|
-
private func decodeCanonicalBase64URL(_ value: String, index: Int) throws -> Data {
|
|
2266
|
+
private static func decodeCanonicalBase64URL(_ value: String, index: Int) throws -> Data {
|
|
2030
2267
|
guard value.utf8.allSatisfy({ byte in
|
|
2031
2268
|
(byte >= 65 && byte <= 90) ||
|
|
2032
2269
|
(byte >= 97 && byte <= 122) ||
|
|
@@ -2051,7 +2288,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2051
2288
|
return data
|
|
2052
2289
|
}
|
|
2053
2290
|
|
|
2054
|
-
private func decodeCanonicalInt64(_ value: String, index: Int) throws -> Int64 {
|
|
2291
|
+
private static func decodeCanonicalInt64(_ value: String, index: Int) throws -> Int64 {
|
|
2055
2292
|
guard value == "0" || value.range(of: "^-?[1-9][0-9]*$", options: .regularExpression) != nil,
|
|
2056
2293
|
let parsed = Int64(value), String(parsed) == value else {
|
|
2057
2294
|
throw BridgeParamError.invalidTag(index: index, reason: "int64 is not canonical or is out of range")
|
|
@@ -2059,46 +2296,53 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2059
2296
|
return parsed
|
|
2060
2297
|
}
|
|
2061
2298
|
|
|
2062
|
-
private func bridgeTagToParam(
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2299
|
+
private static func bridgeTagToParam(
|
|
2300
|
+
fieldCount: Int,
|
|
2301
|
+
stringField: (String) -> String?,
|
|
2302
|
+
containsField: (String) -> Bool,
|
|
2303
|
+
index: Int
|
|
2304
|
+
) throws -> (tag: [String: BridgeValue], param: (any DatabaseValueConvertible)?) {
|
|
2305
|
+
guard fieldCount == 2, let type = stringField("type") else {
|
|
2066
2306
|
throw BridgeParamError.invalidTag(index: index, reason: "tag must contain exactly type and payload fields")
|
|
2067
2307
|
}
|
|
2068
2308
|
|
|
2069
2309
|
switch type {
|
|
2070
2310
|
case "bytes":
|
|
2071
|
-
guard let base64 =
|
|
2311
|
+
guard let base64 = stringField("base64"), !containsField("value") else {
|
|
2072
2312
|
throw BridgeParamError.invalidTag(index: index, reason: "bytes tag must contain base64")
|
|
2073
2313
|
}
|
|
2074
|
-
return
|
|
2314
|
+
return (
|
|
2315
|
+
["type": .string(type), "base64": .string(base64)],
|
|
2316
|
+
try decodeCanonicalBase64URL(base64, index: index)
|
|
2317
|
+
)
|
|
2075
2318
|
case "int64":
|
|
2076
|
-
guard let value =
|
|
2319
|
+
guard let value = stringField("value"), !containsField("base64") else {
|
|
2077
2320
|
throw BridgeParamError.invalidTag(index: index, reason: "int64 tag must contain value")
|
|
2078
2321
|
}
|
|
2079
|
-
return
|
|
2322
|
+
return (
|
|
2323
|
+
["type": .string(type), "value": .string(value)],
|
|
2324
|
+
try decodeCanonicalInt64(value, index: index)
|
|
2325
|
+
)
|
|
2080
2326
|
default:
|
|
2081
2327
|
throw BridgeParamError.invalidTag(index: index, reason: "unknown tag type")
|
|
2082
2328
|
}
|
|
2083
2329
|
}
|
|
2084
2330
|
|
|
2085
|
-
private func bridgeValueToParam(
|
|
2331
|
+
private static func bridgeValueToParam(
|
|
2332
|
+
_ value: BridgeValue,
|
|
2333
|
+
index: Int
|
|
2334
|
+
) throws -> (any DatabaseValueConvertible)? {
|
|
2086
2335
|
switch value {
|
|
2087
|
-
case
|
|
2336
|
+
case .null:
|
|
2088
2337
|
return nil
|
|
2089
|
-
case let boolVal
|
|
2338
|
+
case .bool(let boolVal):
|
|
2090
2339
|
return boolVal ? 1 : 0
|
|
2091
|
-
case let intVal
|
|
2092
|
-
guard
|
|
2340
|
+
case .integer(let intVal):
|
|
2341
|
+
guard intVal >= Self.minSafeInteger && intVal <= Self.maxSafeInteger else {
|
|
2093
2342
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2094
2343
|
}
|
|
2095
2344
|
return intVal
|
|
2096
|
-
case let
|
|
2097
|
-
guard int64Val >= Self.minSafeInteger && int64Val <= Self.maxSafeInteger else {
|
|
2098
|
-
throw BridgeParamError.invalidNumber(index: index)
|
|
2099
|
-
}
|
|
2100
|
-
return int64Val
|
|
2101
|
-
case let doubleVal as Double:
|
|
2345
|
+
case .double(let doubleVal):
|
|
2102
2346
|
guard doubleVal.isFinite else {
|
|
2103
2347
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2104
2348
|
}
|
|
@@ -2107,40 +2351,32 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2107
2351
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2108
2352
|
}
|
|
2109
2353
|
return doubleVal
|
|
2110
|
-
case let stringVal
|
|
2354
|
+
case .string(let stringVal):
|
|
2111
2355
|
return stringVal
|
|
2112
|
-
case let dictionary
|
|
2113
|
-
return try bridgeTagToParam(
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2131
|
-
return doubleValue
|
|
2132
|
-
default:
|
|
2133
|
-
throw BridgeParamError.unsupported(index: index, type: String(describing: type(of: value)))
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
private func bridgeParams(_ values: [Any]) throws -> [(any DatabaseValueConvertible)?] {
|
|
2356
|
+
case .object(let dictionary):
|
|
2357
|
+
return try bridgeTagToParam(
|
|
2358
|
+
fieldCount: dictionary.count,
|
|
2359
|
+
stringField: { field in
|
|
2360
|
+
guard case .string(let value)? = dictionary[field] else { return nil }
|
|
2361
|
+
return value
|
|
2362
|
+
},
|
|
2363
|
+
containsField: { dictionary[$0] != nil },
|
|
2364
|
+
index: index
|
|
2365
|
+
).param
|
|
2366
|
+
case .array:
|
|
2367
|
+
throw BridgeParamError.unsupported(index: index, type: "array")
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private static func bridgeParams(
|
|
2372
|
+
_ values: [BridgeValue]
|
|
2373
|
+
) throws -> [(any DatabaseValueConvertible)?] {
|
|
2138
2374
|
try values.enumerated().map { index, value in
|
|
2139
2375
|
try bridgeValueToParam(value, index: index)
|
|
2140
2376
|
}
|
|
2141
2377
|
}
|
|
2142
2378
|
|
|
2143
|
-
private func parseColumns(_ json: String) throws -> [ColumnDef] {
|
|
2379
|
+
private static func parseColumns(_ json: String) throws -> [ColumnDef] {
|
|
2144
2380
|
struct Column: Decodable {
|
|
2145
2381
|
let name: String
|
|
2146
2382
|
let type: String
|
|
@@ -2169,7 +2405,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2169
2405
|
}
|
|
2170
2406
|
}
|
|
2171
2407
|
|
|
2172
|
-
private func parseTableOptions(_ json: String) throws -> Synchro.TableOptions {
|
|
2408
|
+
private static func parseTableOptions(_ json: String) throws -> Synchro.TableOptions {
|
|
2173
2409
|
struct Options: Decodable {
|
|
2174
2410
|
let ifNotExists: Bool?
|
|
2175
2411
|
let withoutRowid: Bool?
|
|
@@ -2190,34 +2426,34 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2190
2426
|
)
|
|
2191
2427
|
}
|
|
2192
2428
|
|
|
2193
|
-
private func
|
|
2429
|
+
private static func databaseValueToBridgeValue(_ dbValue: DatabaseValue) -> BridgeValue {
|
|
2194
2430
|
switch dbValue.storage {
|
|
2195
2431
|
case .null:
|
|
2196
|
-
return
|
|
2432
|
+
return .null
|
|
2197
2433
|
case .int64(let v):
|
|
2198
2434
|
if v >= Self.minSafeInteger && v <= Self.maxSafeInteger {
|
|
2199
|
-
return
|
|
2435
|
+
return .integer(v)
|
|
2200
2436
|
}
|
|
2201
|
-
return ["type": "int64", "value": String(v)]
|
|
2437
|
+
return .object(["type": .string("int64"), "value": .string(String(v))])
|
|
2202
2438
|
case .double(let v):
|
|
2203
|
-
return
|
|
2439
|
+
return .double(v)
|
|
2204
2440
|
case .string(let v):
|
|
2205
|
-
return v
|
|
2441
|
+
return .string(v)
|
|
2206
2442
|
case .blob(let v):
|
|
2207
|
-
return ["type": "bytes", "base64": canonicalBase64URL(v)]
|
|
2443
|
+
return .object(["type": .string("bytes"), "base64": .string(canonicalBase64URL(v))])
|
|
2208
2444
|
}
|
|
2209
2445
|
}
|
|
2210
2446
|
|
|
2211
|
-
private func
|
|
2212
|
-
var
|
|
2447
|
+
private static func rowToBridgeValue(_ row: Row) -> BridgeValue {
|
|
2448
|
+
var values: [String: BridgeValue] = [:]
|
|
2213
2449
|
for column in row.columnNames {
|
|
2214
2450
|
let dbValue: DatabaseValue = row[column]
|
|
2215
|
-
|
|
2451
|
+
values[column] = databaseValueToBridgeValue(dbValue)
|
|
2216
2452
|
}
|
|
2217
|
-
return
|
|
2453
|
+
return .object(values)
|
|
2218
2454
|
}
|
|
2219
2455
|
|
|
2220
|
-
private func
|
|
2221
|
-
rows.map
|
|
2456
|
+
private static func rowsToBridgeValue(_ rows: [Row]) -> BridgeValue {
|
|
2457
|
+
.array(rows.map(rowToBridgeValue))
|
|
2222
2458
|
}
|
|
2223
2459
|
}
|