@trainstar/synchro-react-native 0.4.0-rc.2 → 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 +40 -10
- package/SynchroReactNative.podspec +3 -1
- package/android/build.gradle +8 -7
- package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/SynchroModuleTransactionTest.kt +7 -10
- package/ios/SynchroModule.swift +953 -713
- 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 +320 -35
- package/ios-tests/TransactionSessionTests.swift +167 -12
- package/lib/module/NativeSynchro.js.map +1 -1
- package/lib/typescript/src/NativeSynchro.d.ts +13 -14
- package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/NativeSynchro.ts +13 -17
- 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
|
}
|
|
@@ -179,6 +234,7 @@ enum TransactionOp {
|
|
|
179
234
|
private struct TransactionTimeoutError: Error {}
|
|
180
235
|
private struct TransactionRollbackError: Error {}
|
|
181
236
|
private struct DatabaseAlreadyExistsError: Error {}
|
|
237
|
+
private struct InvalidAuthoredWriteOperationError: Error {}
|
|
182
238
|
private struct TransactionAbortedError: LocalizedError {
|
|
183
239
|
let message: String
|
|
184
240
|
|
|
@@ -215,50 +271,84 @@ private actor LifecycleMutex {
|
|
|
215
271
|
|
|
216
272
|
@objc(SynchroModuleImpl)
|
|
217
273
|
public class SynchroModuleImpl: NSObject {
|
|
218
|
-
|
|
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
|
+
}
|
|
219
286
|
|
|
220
|
-
private
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
private var statusSubscription: (any Synchro.Cancellable)?
|
|
227
|
-
private var syncEventSubscription: (any Synchro.Cancellable)?
|
|
228
|
-
private var conflictSubscription: (any Synchro.Cancellable)?
|
|
229
|
-
private let statusDetailsLock = NSLock()
|
|
230
|
-
private var cachedBackoff: SyncBackoffEvent?
|
|
231
|
-
private var cachedFailure: SyncFailure?
|
|
232
|
-
private var transportObservations: TransportObservationCollector?
|
|
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
|
+
}
|
|
233
293
|
|
|
234
|
-
private
|
|
235
|
-
private let
|
|
294
|
+
private let eventTarget = ReactEventTarget()
|
|
295
|
+
private let runtime = OSAllocatedUnfairLock(initialState: RuntimeState())
|
|
296
|
+
private let authRequests = AuthRequestStore()
|
|
236
297
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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) }
|
|
241
309
|
}
|
|
310
|
+
private let lifecycleMutex = LifecycleMutex()
|
|
311
|
+
private let databaseBridgeQueue = DispatchQueue(label: "com.trainstar.synchro.react-native.database")
|
|
242
312
|
|
|
243
313
|
@objc public func rejectWithError(_ reject: @escaping RCTPromiseRejectBlock, _ error: Error) {
|
|
314
|
+
Self.rejectWithError(reject, error)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fileprivate static func rejectWithError(_ settlement: ReactPromiseSettlement, _ error: Error) {
|
|
318
|
+
let rejection = rejection(for: error)
|
|
319
|
+
settlement.reject(rejection.code, rejection.message, rejection.error)
|
|
320
|
+
}
|
|
321
|
+
|
|
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?) {
|
|
244
328
|
if let synchroError = error as? SynchroError {
|
|
245
329
|
let (code, userInfo) = mapSynchroError(synchroError)
|
|
246
330
|
let base: [String: Any] = ["message": synchroError.localizedDescription]
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
)
|
|
252
340
|
} else if error is TransactionTimeoutError {
|
|
253
|
-
|
|
341
|
+
return ("TRANSACTION_TIMEOUT", "Transaction timed out due to inactivity", nil)
|
|
254
342
|
} else if error is TransactionAbortedError {
|
|
255
|
-
|
|
343
|
+
return ("NOT_CONNECTED", "Client closed during transaction", error)
|
|
344
|
+
} else if error is InvalidAuthoredWriteOperationError {
|
|
345
|
+
return ("INVALID_CONFIG", "Invalid authored write operation", nil)
|
|
256
346
|
} else {
|
|
257
|
-
|
|
347
|
+
return ("UNKNOWN", error.localizedDescription, error)
|
|
258
348
|
}
|
|
259
349
|
}
|
|
260
350
|
|
|
261
|
-
private func mapSynchroError(_ error: SynchroError) -> (String, [String: Any]) {
|
|
351
|
+
private static func mapSynchroError(_ error: SynchroError) -> (String, [String: Any]) {
|
|
262
352
|
switch error {
|
|
263
353
|
case .notConnected:
|
|
264
354
|
return ("NOT_CONNECTED", [:])
|
|
@@ -316,16 +406,17 @@ public class SynchroModuleImpl: NSObject {
|
|
|
316
406
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
317
407
|
reject: @escaping RCTPromiseRejectBlock
|
|
318
408
|
) {
|
|
409
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
319
410
|
guard let dbPath = config["dbPath"] as? String,
|
|
320
411
|
let serverURL = config["serverURL"] as? String,
|
|
321
412
|
let clientID = config["clientID"] as? String,
|
|
322
413
|
let platform = config["platform"] as? String,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
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)
|
|
326
417
|
return
|
|
327
418
|
}
|
|
328
|
-
let resolvedDbPath = resolveDatabasePath(dbPath)
|
|
419
|
+
let resolvedDbPath = Self.resolveDatabasePath(dbPath)
|
|
329
420
|
|
|
330
421
|
let syncInterval = config["syncInterval"] as? Double ?? 30
|
|
331
422
|
let pushDebounce = config["pushDebounce"] as? Double ?? 0.5
|
|
@@ -336,7 +427,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
336
427
|
let configuredTransportCapacity = config["transportObservationCapacity"] as? Int ?? 0
|
|
337
428
|
let requireNewDatabase = config["requireNewDatabase"] as? Bool ?? false
|
|
338
429
|
if configuredTransportCapacity < 0 || configuredTransportCapacity > 512 {
|
|
339
|
-
reject("INVALID_CONFIG", "Transport observation capacity is invalid", nil)
|
|
430
|
+
settlement.reject("INVALID_CONFIG", "Transport observation capacity is invalid", nil)
|
|
340
431
|
return
|
|
341
432
|
}
|
|
342
433
|
let transportCapacity = configuredTransportCapacity == 0 ? nil : configuredTransportCapacity
|
|
@@ -362,16 +453,24 @@ public class SynchroModuleImpl: NSObject {
|
|
|
362
453
|
}
|
|
363
454
|
|
|
364
455
|
let transportObservations = transportCapacity.map(TransportObservationCollector.init(capacity:))
|
|
456
|
+
let authRequests = authRequests
|
|
457
|
+
let eventTarget = eventTarget
|
|
365
458
|
let synchroConfig = SynchroConfig(
|
|
366
459
|
dbPath: resolvedDbPath,
|
|
367
460
|
serverURL: url,
|
|
368
|
-
authProvider: {
|
|
461
|
+
authProvider: {
|
|
369
462
|
try await withCheckedThrowingContinuation { continuation in
|
|
370
463
|
let requestID = UUID().uuidString
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
)
|
|
375
474
|
}
|
|
376
475
|
},
|
|
377
476
|
clientID: clientID,
|
|
@@ -385,33 +484,43 @@ public class SynchroModuleImpl: NSObject {
|
|
|
385
484
|
seedDatabasePath: resolvedSeedPath,
|
|
386
485
|
transportObservationCollector: transportObservations
|
|
387
486
|
)
|
|
388
|
-
|
|
389
|
-
|
|
487
|
+
let lifecycleMutex = lifecycleMutex
|
|
488
|
+
let runtime = runtime
|
|
489
|
+
let databaseBridgeQueue = databaseBridgeQueue
|
|
490
|
+
Task {
|
|
390
491
|
do {
|
|
391
|
-
try await
|
|
392
|
-
await
|
|
393
|
-
|
|
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()
|
|
394
499
|
if requireNewDatabase && FileManager.default.fileExists(atPath: resolvedDbPath) {
|
|
395
500
|
throw DatabaseAlreadyExistsError()
|
|
396
501
|
}
|
|
397
502
|
let client = try SynchroClient(config: synchroConfig)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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)
|
|
401
510
|
}
|
|
402
511
|
DispatchQueue.main.async {
|
|
403
|
-
resolve(
|
|
512
|
+
settlement.resolve(.void)
|
|
404
513
|
}
|
|
405
514
|
} catch is DatabaseAlreadyExistsError {
|
|
406
515
|
DispatchQueue.main.async {
|
|
407
|
-
reject("INVALID_CONFIG", "Database already exists", nil)
|
|
516
|
+
settlement.reject("INVALID_CONFIG", "Database already exists", nil)
|
|
408
517
|
}
|
|
409
518
|
} catch {
|
|
410
519
|
DispatchQueue.main.async {
|
|
411
520
|
if resolvedSeedPath != nil {
|
|
412
|
-
reject("INVALID_SEED", "Seed database failed validation", error)
|
|
521
|
+
settlement.reject("INVALID_SEED", "Seed database failed validation", error)
|
|
413
522
|
} else {
|
|
414
|
-
|
|
523
|
+
Self.rejectWithError(settlement, error)
|
|
415
524
|
}
|
|
416
525
|
}
|
|
417
526
|
}
|
|
@@ -420,18 +529,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
420
529
|
|
|
421
530
|
@objc
|
|
422
531
|
public func resolveAuthRequest(_ requestID: String, token: String) {
|
|
423
|
-
|
|
424
|
-
let continuation = pendingAuthContinuations.removeValue(forKey: requestID)
|
|
425
|
-
authLock.unlock()
|
|
426
|
-
continuation?.resume(returning: token)
|
|
532
|
+
authRequests.resolve(requestID, token: token)
|
|
427
533
|
}
|
|
428
534
|
|
|
429
535
|
@objc
|
|
430
536
|
public func rejectAuthRequest(_ requestID: String, error: String) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
537
|
+
authRequests.reject(
|
|
538
|
+
requestID,
|
|
539
|
+
error: NSError(domain: "Auth", code: 0, userInfo: [NSLocalizedDescriptionKey: error])
|
|
540
|
+
)
|
|
435
541
|
}
|
|
436
542
|
|
|
437
543
|
@objc
|
|
@@ -439,20 +545,31 @@ public class SynchroModuleImpl: NSObject {
|
|
|
439
545
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
440
546
|
reject: @escaping RCTPromiseRejectBlock
|
|
441
547
|
) {
|
|
442
|
-
|
|
443
|
-
|
|
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 {
|
|
444
554
|
do {
|
|
445
|
-
try await
|
|
446
|
-
await
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}
|
|
449
566
|
}
|
|
450
567
|
DispatchQueue.main.async {
|
|
451
|
-
resolve(
|
|
568
|
+
settlement.resolve(.void)
|
|
452
569
|
}
|
|
453
570
|
} catch {
|
|
454
571
|
DispatchQueue.main.async {
|
|
455
|
-
|
|
572
|
+
Self.rejectWithError(settlement, error)
|
|
456
573
|
}
|
|
457
574
|
}
|
|
458
575
|
}
|
|
@@ -463,14 +580,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
463
580
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
464
581
|
reject: @escaping RCTPromiseRejectBlock
|
|
465
582
|
) {
|
|
466
|
-
guard let client = client else {
|
|
583
|
+
guard let client = runtime.withLock({ $0.client }) else {
|
|
467
584
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
468
585
|
return
|
|
469
586
|
}
|
|
470
587
|
resolve(client.path)
|
|
471
588
|
}
|
|
472
589
|
|
|
473
|
-
private func resolveDatabasePath(_ dbPath: String) -> String {
|
|
590
|
+
private static func resolveDatabasePath(_ dbPath: String) -> String {
|
|
474
591
|
if (dbPath as NSString).isAbsolutePath {
|
|
475
592
|
return dbPath
|
|
476
593
|
}
|
|
@@ -487,16 +604,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
487
604
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
488
605
|
reject: @escaping RCTPromiseRejectBlock
|
|
489
606
|
) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
rejectWithError(reject, error)
|
|
499
|
-
}
|
|
607
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
608
|
+
submitDatabaseOperation(
|
|
609
|
+
resolve: resolve,
|
|
610
|
+
reject: reject,
|
|
611
|
+
operation: { client in
|
|
612
|
+
.value(try Self.rowsToBridgeValue(client.query(sql, params: Self.bridgeParams(capturedParams.get()))))
|
|
613
|
+
}
|
|
614
|
+
)
|
|
500
615
|
}
|
|
501
616
|
|
|
502
617
|
@objc
|
|
@@ -506,20 +621,17 @@ public class SynchroModuleImpl: NSObject {
|
|
|
506
621
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
507
622
|
reject: @escaping RCTPromiseRejectBlock
|
|
508
623
|
) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
resolve(NSNull())
|
|
624
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
625
|
+
submitDatabaseOperation(
|
|
626
|
+
resolve: resolve,
|
|
627
|
+
reject: reject,
|
|
628
|
+
operation: { client in
|
|
629
|
+
if let row = try client.queryOne(sql, params: Self.bridgeParams(capturedParams.get())) {
|
|
630
|
+
return .value(Self.rowToBridgeValue(row))
|
|
631
|
+
}
|
|
632
|
+
return .value(.null)
|
|
519
633
|
}
|
|
520
|
-
|
|
521
|
-
rejectWithError(reject, error)
|
|
522
|
-
}
|
|
634
|
+
)
|
|
523
635
|
}
|
|
524
636
|
|
|
525
637
|
@objc
|
|
@@ -529,16 +641,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
529
641
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
530
642
|
reject: @escaping RCTPromiseRejectBlock
|
|
531
643
|
) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
644
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
645
|
+
submitDatabaseOperation(
|
|
646
|
+
resolve: resolve,
|
|
647
|
+
reject: reject,
|
|
648
|
+
operation: { client in
|
|
649
|
+
let result = try client.execute(sql, params: Self.bridgeParams(capturedParams.get()))
|
|
650
|
+
return .value(.object(["rowsAffected": .integer(Int64(result.rowsAffected))]))
|
|
651
|
+
}
|
|
652
|
+
)
|
|
542
653
|
}
|
|
543
654
|
|
|
544
655
|
@objc
|
|
@@ -551,27 +662,25 @@ public class SynchroModuleImpl: NSObject {
|
|
|
551
662
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
552
663
|
reject: @escaping RCTPromiseRejectBlock
|
|
553
664
|
) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
665
|
+
let capturedValues = Result { try Self.captureSQLParams(values) }
|
|
666
|
+
submitDatabaseOperation(
|
|
667
|
+
resolve: resolve,
|
|
668
|
+
reject: reject,
|
|
669
|
+
operation: { client in
|
|
670
|
+
guard let authoredOperation = Synchro.Operation(rawValue: operation) else {
|
|
671
|
+
throw InvalidAuthoredWriteOperationError()
|
|
672
|
+
}
|
|
673
|
+
let params = try Self.bridgeParams(capturedValues.get())
|
|
674
|
+
let result = try client.authoredWriteTransaction(
|
|
675
|
+
tableName: tableName,
|
|
676
|
+
operation: authoredOperation,
|
|
677
|
+
columnNames: columnNames
|
|
678
|
+
) { transaction in
|
|
679
|
+
try transaction.execute(sql, params: params)
|
|
680
|
+
}
|
|
681
|
+
return .value(.object(["rowsAffected": .integer(Int64(result.rowsAffected))]))
|
|
570
682
|
}
|
|
571
|
-
|
|
572
|
-
} catch {
|
|
573
|
-
rejectWithError(reject, error)
|
|
574
|
-
}
|
|
683
|
+
)
|
|
575
684
|
}
|
|
576
685
|
|
|
577
686
|
@objc
|
|
@@ -580,16 +689,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
580
689
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
581
690
|
reject: @escaping RCTPromiseRejectBlock
|
|
582
691
|
) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
692
|
+
let capturedStatements = Result { try Self.captureBridgeStatements(statements) }
|
|
693
|
+
submitDatabaseOperation(
|
|
694
|
+
resolve: resolve,
|
|
695
|
+
reject: reject,
|
|
696
|
+
operation: { client in
|
|
697
|
+
let total = try client.executeBatch(try Self.bridgeStatements(capturedStatements.get()))
|
|
698
|
+
return .value(.object(["totalRowsAffected": .integer(Int64(total))]))
|
|
699
|
+
}
|
|
700
|
+
)
|
|
593
701
|
}
|
|
594
702
|
|
|
595
703
|
// MARK: - Transactions
|
|
@@ -618,7 +726,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
618
726
|
beginTransaction(isWrite: false, resolve: resolve, reject: reject)
|
|
619
727
|
}
|
|
620
728
|
|
|
621
|
-
private func runTransactionLoop(
|
|
729
|
+
private static func runTransactionLoop(
|
|
622
730
|
session: TransactionSession,
|
|
623
731
|
txID: String,
|
|
624
732
|
query: (String, [(any DatabaseValueConvertible)?]) throws -> [Row],
|
|
@@ -633,60 +741,60 @@ public class SynchroModuleImpl: NSObject {
|
|
|
633
741
|
}
|
|
634
742
|
|
|
635
743
|
switch op {
|
|
636
|
-
case .query(let sql, let params, let
|
|
744
|
+
case .query(let sql, let params, let settlement):
|
|
637
745
|
do {
|
|
638
|
-
let rows = try query(sql, try bridgeParams(params))
|
|
746
|
+
let rows = try query(sql, try bridgeParams(params.get()))
|
|
639
747
|
if let abortError = session.currentAbortError() {
|
|
640
748
|
throw abortError
|
|
641
749
|
}
|
|
642
|
-
|
|
750
|
+
settlement.resolve(.value(rowsToBridgeValue(rows)))
|
|
643
751
|
} catch {
|
|
644
|
-
|
|
752
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
645
753
|
}
|
|
646
754
|
|
|
647
|
-
case .queryOne(let sql, let params, let
|
|
755
|
+
case .queryOne(let sql, let params, let settlement):
|
|
648
756
|
do {
|
|
649
|
-
let row = try queryOne(sql, try bridgeParams(params))
|
|
757
|
+
let row = try queryOne(sql, try bridgeParams(params.get()))
|
|
650
758
|
if let abortError = session.currentAbortError() {
|
|
651
759
|
throw abortError
|
|
652
760
|
}
|
|
653
|
-
|
|
761
|
+
settlement.resolve(.value(row.map(rowToBridgeValue) ?? .null))
|
|
654
762
|
} catch {
|
|
655
|
-
|
|
763
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
656
764
|
}
|
|
657
765
|
|
|
658
|
-
case .execute(let sql, let params, let
|
|
766
|
+
case .execute(let sql, let params, let settlement):
|
|
659
767
|
do {
|
|
660
768
|
guard let execute else {
|
|
661
769
|
throw TransactionReadOnlyError()
|
|
662
770
|
}
|
|
663
|
-
let rowsAffected = try execute(sql, try bridgeParams(params))
|
|
771
|
+
let rowsAffected = try execute(sql, try bridgeParams(params.get()))
|
|
664
772
|
if let abortError = session.currentAbortError() {
|
|
665
773
|
throw abortError
|
|
666
774
|
}
|
|
667
|
-
|
|
775
|
+
settlement.resolve(.value(.object(["rowsAffected": .integer(Int64(rowsAffected))])))
|
|
668
776
|
} catch {
|
|
669
|
-
|
|
777
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
670
778
|
}
|
|
671
779
|
|
|
672
|
-
case .commit(let
|
|
780
|
+
case .commit(let settlement):
|
|
673
781
|
guard session.close() else {
|
|
674
782
|
let error = session.currentAbortError()
|
|
675
783
|
?? TransactionAbortedError(message: "Transaction already completed")
|
|
676
|
-
|
|
784
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
677
785
|
throw error
|
|
678
786
|
}
|
|
679
|
-
session.
|
|
787
|
+
session.setFinalSettlement(.commit(settlement))
|
|
680
788
|
return
|
|
681
789
|
|
|
682
|
-
case .rollback(let
|
|
790
|
+
case .rollback(let settlement):
|
|
683
791
|
guard session.close() else {
|
|
684
792
|
let error = session.currentAbortError()
|
|
685
793
|
?? TransactionAbortedError(message: "Transaction already completed")
|
|
686
|
-
|
|
794
|
+
settlement.reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
687
795
|
throw error
|
|
688
796
|
}
|
|
689
|
-
session.
|
|
797
|
+
session.setFinalSettlement(.rollback(settlement))
|
|
690
798
|
throw TransactionRollbackError()
|
|
691
799
|
}
|
|
692
800
|
}
|
|
@@ -698,36 +806,31 @@ public class SynchroModuleImpl: NSObject {
|
|
|
698
806
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
699
807
|
reject: @escaping RCTPromiseRejectBlock
|
|
700
808
|
) {
|
|
701
|
-
|
|
702
|
-
guard acceptingTransactions, let client = client else {
|
|
703
|
-
sessionsLock.unlock()
|
|
704
|
-
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
|
|
809
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
708
810
|
let txID = UUID().uuidString
|
|
709
|
-
let session = TransactionSession(isWrite: isWrite
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
|
714
820
|
}
|
|
715
|
-
sessions[txID] = session
|
|
716
|
-
sessionsLock.unlock()
|
|
717
821
|
|
|
822
|
+
let runtime = runtime
|
|
718
823
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
719
824
|
defer {
|
|
720
825
|
session.close()
|
|
721
|
-
|
|
722
|
-
self.sessions.removeValue(forKey: txID)
|
|
723
|
-
self.sessionsLock.unlock()
|
|
826
|
+
_ = runtime.withLock { $0.sessions.removeValue(forKey: txID) }
|
|
724
827
|
session.markFinished()
|
|
725
828
|
}
|
|
726
829
|
do {
|
|
727
830
|
let finalResult: Result<Void, Error>
|
|
728
831
|
if isWrite {
|
|
729
832
|
let body: (ApplicationTransaction) throws -> Void = { transaction in
|
|
730
|
-
try
|
|
833
|
+
try Self.runTransactionLoop(
|
|
731
834
|
session: session,
|
|
732
835
|
txID: txID,
|
|
733
836
|
query: { sql, params in
|
|
@@ -748,7 +851,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
748
851
|
}
|
|
749
852
|
} else {
|
|
750
853
|
try client.readTransaction { db in
|
|
751
|
-
try
|
|
854
|
+
try Self.runTransactionLoop(
|
|
752
855
|
session: session,
|
|
753
856
|
txID: txID,
|
|
754
857
|
query: { sql, params in
|
|
@@ -797,18 +900,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
797
900
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
798
901
|
reject: @escaping RCTPromiseRejectBlock
|
|
799
902
|
) {
|
|
903
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
800
904
|
guard let session = getSession(txID) else {
|
|
801
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
905
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
802
906
|
return
|
|
803
907
|
}
|
|
804
|
-
let
|
|
805
|
-
|
|
806
|
-
case .success(let rows): resolve(rows)
|
|
807
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
808
|
-
}
|
|
809
|
-
}
|
|
908
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
909
|
+
let op = TransactionOp.query(sql: sql, params: capturedParams, settlement: settlement)
|
|
810
910
|
guard session.enqueue(op) else {
|
|
811
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
911
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
812
912
|
return
|
|
813
913
|
}
|
|
814
914
|
}
|
|
@@ -821,18 +921,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
821
921
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
822
922
|
reject: @escaping RCTPromiseRejectBlock
|
|
823
923
|
) {
|
|
924
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
824
925
|
guard let session = getSession(txID) else {
|
|
825
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
926
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
826
927
|
return
|
|
827
928
|
}
|
|
828
|
-
let
|
|
829
|
-
|
|
830
|
-
case .success(let row): resolve(row ?? NSNull())
|
|
831
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
832
|
-
}
|
|
833
|
-
}
|
|
929
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
930
|
+
let op = TransactionOp.queryOne(sql: sql, params: capturedParams, settlement: settlement)
|
|
834
931
|
guard session.enqueue(op) else {
|
|
835
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
932
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
836
933
|
return
|
|
837
934
|
}
|
|
838
935
|
}
|
|
@@ -845,22 +942,19 @@ public class SynchroModuleImpl: NSObject {
|
|
|
845
942
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
846
943
|
reject: @escaping RCTPromiseRejectBlock
|
|
847
944
|
) {
|
|
945
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
848
946
|
guard let session = getSession(txID) else {
|
|
849
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
947
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
850
948
|
return
|
|
851
949
|
}
|
|
852
950
|
guard session.isWrite else {
|
|
853
|
-
reject("DATABASE_ERROR", "Read transactions cannot execute SQL", nil)
|
|
951
|
+
settlement.reject("DATABASE_ERROR", "Read transactions cannot execute SQL", nil)
|
|
854
952
|
return
|
|
855
953
|
}
|
|
856
|
-
let
|
|
857
|
-
|
|
858
|
-
case .success(let dict): resolve(dict)
|
|
859
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
860
|
-
}
|
|
861
|
-
}
|
|
954
|
+
let capturedParams = Result { try Self.captureSQLParams(params) }
|
|
955
|
+
let op = TransactionOp.execute(sql: sql, params: capturedParams, settlement: settlement)
|
|
862
956
|
guard session.enqueue(op) else {
|
|
863
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
957
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
864
958
|
return
|
|
865
959
|
}
|
|
866
960
|
}
|
|
@@ -871,19 +965,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
871
965
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
872
966
|
reject: @escaping RCTPromiseRejectBlock
|
|
873
967
|
) {
|
|
968
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
874
969
|
guard let session = getSession(txID) else {
|
|
875
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
970
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
876
971
|
return
|
|
877
972
|
}
|
|
878
|
-
let op = TransactionOp.commit
|
|
879
|
-
switch result {
|
|
880
|
-
case .success: resolve(nil)
|
|
881
|
-
case .failure(let error as SynchroError): self.rejectWithError(reject, error)
|
|
882
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
883
|
-
}
|
|
884
|
-
}
|
|
973
|
+
let op = TransactionOp.commit(settlement: settlement)
|
|
885
974
|
guard session.enqueue(op) else {
|
|
886
|
-
reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
975
|
+
settlement.reject("TRANSACTION_TIMEOUT", "Transaction not found or expired", nil)
|
|
887
976
|
return
|
|
888
977
|
}
|
|
889
978
|
}
|
|
@@ -894,128 +983,161 @@ public class SynchroModuleImpl: NSObject {
|
|
|
894
983
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
895
984
|
reject: @escaping RCTPromiseRejectBlock
|
|
896
985
|
) {
|
|
986
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
897
987
|
guard let session = getSession(txID) else {
|
|
898
|
-
resolve(
|
|
988
|
+
settlement.resolve(.void)
|
|
899
989
|
return
|
|
900
990
|
}
|
|
901
|
-
let op = TransactionOp.rollback
|
|
902
|
-
switch result {
|
|
903
|
-
case .success: resolve(nil)
|
|
904
|
-
case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
|
|
905
|
-
}
|
|
906
|
-
}
|
|
991
|
+
let op = TransactionOp.rollback(settlement: settlement)
|
|
907
992
|
guard session.enqueue(op) else {
|
|
908
|
-
resolve(
|
|
993
|
+
settlement.resolve(.void)
|
|
909
994
|
return
|
|
910
995
|
}
|
|
911
996
|
}
|
|
912
997
|
|
|
913
998
|
private func getSession(_ txID: String) -> TransactionSession? {
|
|
914
|
-
|
|
915
|
-
defer { sessionsLock.unlock() }
|
|
916
|
-
return sessions[txID]
|
|
999
|
+
runtime.withLock { $0.sessions[txID] }
|
|
917
1000
|
}
|
|
918
1001
|
|
|
919
|
-
private func wireClientEvents(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
}
|
|
927
1011
|
|
|
928
|
-
statusSubscription = client.onStatusChange {
|
|
1012
|
+
let statusSubscription = client.onStatusChange { status in
|
|
929
1013
|
guard status != .backoff, status != .error else { return }
|
|
930
|
-
|
|
1014
|
+
eventTarget.emit(
|
|
1015
|
+
"onStatusChange",
|
|
1016
|
+
body: bridgeValue(statusPayload(status, client: client, runtime: runtime))
|
|
1017
|
+
)
|
|
931
1018
|
}
|
|
932
1019
|
|
|
933
|
-
syncEventSubscription = client.onSyncEvent {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
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)))
|
|
937
1023
|
switch event {
|
|
938
1024
|
case .backoff, .failure:
|
|
939
|
-
|
|
1025
|
+
eventTarget.emit(
|
|
940
1026
|
"onStatusChange",
|
|
941
|
-
|
|
1027
|
+
body: bridgeValue(statusPayload(client.getSyncStatus(), client: client, runtime: runtime))
|
|
942
1028
|
)
|
|
943
1029
|
default:
|
|
944
1030
|
break
|
|
945
1031
|
}
|
|
946
1032
|
}
|
|
947
1033
|
|
|
948
|
-
conflictSubscription = client.onConflict {
|
|
949
|
-
|
|
1034
|
+
let conflictSubscription = client.onConflict { event in
|
|
1035
|
+
eventTarget.emit("onConflict", body: bridgeValue(conflictPayload(event)))
|
|
950
1036
|
}
|
|
951
|
-
}
|
|
952
1037
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1038
|
+
runtime.withLock {
|
|
1039
|
+
$0.statusSubscription = statusSubscription
|
|
1040
|
+
$0.syncEventSubscription = syncEventSubscription
|
|
1041
|
+
$0.conflictSubscription = conflictSubscription
|
|
1042
|
+
}
|
|
958
1043
|
}
|
|
959
1044
|
|
|
960
|
-
private func withLifecycleLock(
|
|
961
|
-
|
|
1045
|
+
private static func withLifecycleLock(
|
|
1046
|
+
_ mutex: LifecycleMutex,
|
|
1047
|
+
operation: @escaping @Sendable () async throws -> Void
|
|
1048
|
+
) async throws {
|
|
1049
|
+
await mutex.lock()
|
|
962
1050
|
do {
|
|
963
1051
|
try await operation()
|
|
964
|
-
await
|
|
1052
|
+
await mutex.unlock()
|
|
965
1053
|
} catch {
|
|
966
|
-
await
|
|
1054
|
+
await mutex.unlock()
|
|
967
1055
|
throw error
|
|
968
1056
|
}
|
|
969
1057
|
}
|
|
970
1058
|
|
|
971
|
-
private func
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
let authContinuations = pendingAuthContinuations
|
|
996
|
-
pendingAuthContinuations.removeAll()
|
|
997
|
-
authLock.unlock()
|
|
998
|
-
authContinuations.values.forEach { continuation in
|
|
999
|
-
continuation.resume(throwing: NSError(
|
|
1000
|
-
domain: "SynchroModule",
|
|
1001
|
-
code: 0,
|
|
1002
|
-
userInfo: [NSLocalizedDescriptionKey: "client closed"]
|
|
1003
|
-
))
|
|
1059
|
+
private func submitDatabaseOperation(
|
|
1060
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
1061
|
+
reject: @escaping RCTPromiseRejectBlock,
|
|
1062
|
+
operation: @escaping @Sendable (SynchroClient) throws -> BridgePromiseValue
|
|
1063
|
+
) {
|
|
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
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return true
|
|
1078
|
+
}
|
|
1079
|
+
admissionGate.signal()
|
|
1080
|
+
guard admitted else {
|
|
1081
|
+
settlement.reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1082
|
+
return
|
|
1004
1083
|
}
|
|
1005
|
-
return activeSessions
|
|
1006
1084
|
}
|
|
1007
1085
|
|
|
1008
|
-
private func
|
|
1009
|
-
|
|
1010
|
-
|
|
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 {
|
|
1011
1124
|
$0.abort(TransactionAbortedError(message: "Client closed during transaction"))
|
|
1012
1125
|
}
|
|
1013
|
-
for session in
|
|
1126
|
+
for session in detached.sessions {
|
|
1014
1127
|
await session.waitUntilFinished()
|
|
1015
1128
|
}
|
|
1129
|
+
await withCheckedContinuation { continuation in
|
|
1130
|
+
databaseBridgeQueue.async {
|
|
1131
|
+
continuation.resume()
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1016
1134
|
}
|
|
1017
1135
|
|
|
1018
|
-
private func statusPayload(
|
|
1136
|
+
private static func statusPayload(
|
|
1137
|
+
_ status: SyncStatus,
|
|
1138
|
+
client: SynchroClient,
|
|
1139
|
+
runtime: OSAllocatedUnfairLock<RuntimeState>
|
|
1140
|
+
) -> [String: Any] {
|
|
1019
1141
|
var payload: [String: Any] = [
|
|
1020
1142
|
"status": status.rawValue,
|
|
1021
1143
|
"retryAt": NSNull(),
|
|
@@ -1026,23 +1148,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1026
1148
|
case .uninitialized, .localReady, .connecting, .schemaApplying, .ready, .pushing, .pulling, .rebuilding, .stopped:
|
|
1027
1149
|
break
|
|
1028
1150
|
case .backoff:
|
|
1029
|
-
|
|
1030
|
-
let backoff = cachedBackoff
|
|
1031
|
-
statusDetailsLock.unlock()
|
|
1151
|
+
let backoff = runtime.withLock { $0.cachedBackoff }
|
|
1032
1152
|
if let backoff {
|
|
1033
1153
|
payload["retryAt"] = iso8601String(backoff.retryAt)
|
|
1034
1154
|
payload["operation"] = backoff.operation.rawValue
|
|
1035
1155
|
}
|
|
1036
1156
|
case .error:
|
|
1037
|
-
|
|
1038
|
-
let cachedFailure = self.cachedFailure
|
|
1039
|
-
statusDetailsLock.unlock()
|
|
1157
|
+
let cachedFailure = runtime.withLock { $0.cachedFailure }
|
|
1040
1158
|
let persistedFailure: SyncFailure?
|
|
1041
|
-
|
|
1042
|
-
persistedFailure = try? client.getBlockingFailure()
|
|
1043
|
-
} else {
|
|
1044
|
-
persistedFailure = nil
|
|
1045
|
-
}
|
|
1159
|
+
persistedFailure = try? client.getBlockingFailure()
|
|
1046
1160
|
if let failure = cachedFailure ?? persistedFailure {
|
|
1047
1161
|
payload["failure"] = failurePayload(failure)
|
|
1048
1162
|
}
|
|
@@ -1050,26 +1164,29 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1050
1164
|
return payload
|
|
1051
1165
|
}
|
|
1052
1166
|
|
|
1053
|
-
private func recordStatusDetails(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
+
}
|
|
1065
1182
|
}
|
|
1066
1183
|
}
|
|
1067
1184
|
|
|
1068
|
-
private func schemaPayload(_ schema: SchemaRef) -> [String: Any] {
|
|
1185
|
+
private static func schemaPayload(_ schema: SchemaRef) -> [String: Any] {
|
|
1069
1186
|
["version": schema.version, "hash": schema.hash]
|
|
1070
1187
|
}
|
|
1071
1188
|
|
|
1072
|
-
private func failurePayload(_ failure: SyncFailure) -> [String: Any] {
|
|
1189
|
+
private static func failurePayload(_ failure: SyncFailure) -> [String: Any] {
|
|
1073
1190
|
[
|
|
1074
1191
|
"operation": failure.operation.rawValue,
|
|
1075
1192
|
"code": failure.code.rawValue,
|
|
@@ -1080,13 +1197,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1080
1197
|
]
|
|
1081
1198
|
}
|
|
1082
1199
|
|
|
1083
|
-
private func iso8601String(_ date: Date) -> String {
|
|
1200
|
+
private static func iso8601String(_ date: Date) -> String {
|
|
1084
1201
|
let formatter = ISO8601DateFormatter()
|
|
1085
1202
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
1086
1203
|
return formatter.string(from: date)
|
|
1087
1204
|
}
|
|
1088
1205
|
|
|
1089
|
-
private func syncEventPayload(_ event: SyncEvent) -> [String: Any] {
|
|
1206
|
+
private static func syncEventPayload(_ event: SyncEvent) -> [String: Any] {
|
|
1090
1207
|
var payload: [String: Any] = [
|
|
1091
1208
|
"type": "",
|
|
1092
1209
|
"from": NSNull(),
|
|
@@ -1153,7 +1270,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1153
1270
|
return payload
|
|
1154
1271
|
}
|
|
1155
1272
|
|
|
1156
|
-
private func conflictPayload(_ event: ConflictEvent) -> [String: Any] {
|
|
1273
|
+
private static func conflictPayload(_ event: ConflictEvent) -> [String: Any] {
|
|
1157
1274
|
[
|
|
1158
1275
|
"table": event.table,
|
|
1159
1276
|
"recordID": event.recordID,
|
|
@@ -1162,7 +1279,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1162
1279
|
]
|
|
1163
1280
|
}
|
|
1164
1281
|
|
|
1165
|
-
private func encodeRejectedMutations(_ results: [RejectedMutation]) -> String {
|
|
1282
|
+
private static func encodeRejectedMutations(_ results: [RejectedMutation]) -> String {
|
|
1166
1283
|
let payload = results.map { result in
|
|
1167
1284
|
[
|
|
1168
1285
|
"mutationID": result.mutationID,
|
|
@@ -1185,12 +1302,12 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1185
1302
|
return json
|
|
1186
1303
|
}
|
|
1187
1304
|
|
|
1188
|
-
private func anyCodableMapToJSONObject(_ value: [String: AnyCodable]?) -> Any {
|
|
1305
|
+
private static func anyCodableMapToJSONObject(_ value: [String: AnyCodable]?) -> Any {
|
|
1189
1306
|
guard let value else { return NSNull() }
|
|
1190
1307
|
return value.mapValues { anyCodableToJSONObject($0.value) }
|
|
1191
1308
|
}
|
|
1192
1309
|
|
|
1193
|
-
private func anyCodableToJSONObject(_ value: Any?) -> Any {
|
|
1310
|
+
private static func anyCodableToJSONObject(_ value: Any?) -> Any {
|
|
1194
1311
|
switch value {
|
|
1195
1312
|
case nil:
|
|
1196
1313
|
return NSNull()
|
|
@@ -1217,7 +1334,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1217
1334
|
}
|
|
1218
1335
|
}
|
|
1219
1336
|
|
|
1220
|
-
private func encodeAnyCodableMap(_ value: [String: AnyCodable]?) -> String? {
|
|
1337
|
+
private static func encodeAnyCodableMap(_ value: [String: AnyCodable]?) -> String? {
|
|
1221
1338
|
guard let value else { return nil }
|
|
1222
1339
|
let encoder = JSONEncoder()
|
|
1223
1340
|
guard let data = try? encoder.encode(value) else { return nil }
|
|
@@ -1234,18 +1351,16 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1234
1351
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1235
1352
|
reject: @escaping RCTPromiseRejectBlock
|
|
1236
1353
|
) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
rejectWithError(reject, error)
|
|
1248
|
-
}
|
|
1354
|
+
submitDatabaseOperation(
|
|
1355
|
+
resolve: resolve,
|
|
1356
|
+
reject: reject,
|
|
1357
|
+
operation: { client in
|
|
1358
|
+
let columns = try Self.parseColumns(columnsJson)
|
|
1359
|
+
let options = try optionsJson.flatMap { try Self.parseTableOptions($0) }
|
|
1360
|
+
try client.createTable(name, columns: columns, options: options)
|
|
1361
|
+
return .void
|
|
1362
|
+
}
|
|
1363
|
+
)
|
|
1249
1364
|
}
|
|
1250
1365
|
|
|
1251
1366
|
@objc
|
|
@@ -1255,17 +1370,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1255
1370
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1256
1371
|
reject: @escaping RCTPromiseRejectBlock
|
|
1257
1372
|
) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
rejectWithError(reject, error)
|
|
1268
|
-
}
|
|
1373
|
+
submitDatabaseOperation(
|
|
1374
|
+
resolve: resolve,
|
|
1375
|
+
reject: reject,
|
|
1376
|
+
operation: { client in
|
|
1377
|
+
let columns = try Self.parseColumns(columnsJson)
|
|
1378
|
+
try client.alterTable(name, addColumns: columns)
|
|
1379
|
+
return .void
|
|
1380
|
+
}
|
|
1381
|
+
)
|
|
1269
1382
|
}
|
|
1270
1383
|
|
|
1271
1384
|
@objc
|
|
@@ -1276,16 +1389,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1276
1389
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1277
1390
|
reject: @escaping RCTPromiseRejectBlock
|
|
1278
1391
|
) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
rejectWithError(reject, error)
|
|
1288
|
-
}
|
|
1392
|
+
submitDatabaseOperation(
|
|
1393
|
+
resolve: resolve,
|
|
1394
|
+
reject: reject,
|
|
1395
|
+
operation: { client in
|
|
1396
|
+
try client.createIndex(table, columns: columns, unique: unique)
|
|
1397
|
+
return .void
|
|
1398
|
+
}
|
|
1399
|
+
)
|
|
1289
1400
|
}
|
|
1290
1401
|
|
|
1291
1402
|
// MARK: - Observation
|
|
@@ -1297,14 +1408,29 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1297
1408
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1298
1409
|
reject: @escaping RCTPromiseRejectBlock
|
|
1299
1410
|
) {
|
|
1300
|
-
guard let client =
|
|
1411
|
+
guard let client = runtime.withLock({ state in
|
|
1412
|
+
state.acceptingTransactions ? state.client : nil
|
|
1413
|
+
}) else {
|
|
1301
1414
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1302
1415
|
return
|
|
1303
1416
|
}
|
|
1304
|
-
let
|
|
1305
|
-
|
|
1417
|
+
let eventTarget = eventTarget
|
|
1418
|
+
let cancellable = client.onChange(tables: tables) {
|
|
1419
|
+
eventTarget.emit(
|
|
1420
|
+
"onChange",
|
|
1421
|
+
body: .object(["observerID": .string(observerID)])
|
|
1422
|
+
)
|
|
1306
1423
|
}
|
|
1307
|
-
|
|
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
|
|
1432
|
+
}
|
|
1433
|
+
registration.1?.cancel()
|
|
1308
1434
|
resolve(nil)
|
|
1309
1435
|
}
|
|
1310
1436
|
|
|
@@ -1317,18 +1443,34 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1317
1443
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1318
1444
|
reject: @escaping RCTPromiseRejectBlock
|
|
1319
1445
|
) {
|
|
1320
|
-
guard let client =
|
|
1446
|
+
guard let client = runtime.withLock({ state in
|
|
1447
|
+
state.acceptingTransactions ? state.client : nil
|
|
1448
|
+
}) else {
|
|
1321
1449
|
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1322
1450
|
return
|
|
1323
1451
|
}
|
|
1324
1452
|
do {
|
|
1325
|
-
let nativeParams = try bridgeParams(params)
|
|
1326
|
-
let
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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))
|
|
1330
1467
|
}
|
|
1331
|
-
|
|
1468
|
+
guard registration.0 else {
|
|
1469
|
+
cancellable.cancel()
|
|
1470
|
+
reject("NOT_CONNECTED", "Client not initialized", nil)
|
|
1471
|
+
return
|
|
1472
|
+
}
|
|
1473
|
+
registration.1?.cancel()
|
|
1332
1474
|
resolve(nil)
|
|
1333
1475
|
} catch {
|
|
1334
1476
|
rejectWithError(reject, error)
|
|
@@ -1341,9 +1483,8 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1341
1483
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1342
1484
|
reject: @escaping RCTPromiseRejectBlock
|
|
1343
1485
|
) {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
}
|
|
1486
|
+
let cancellable = runtime.withLock { $0.observers.removeValue(forKey: observerID) }
|
|
1487
|
+
cancellable?.cancel()
|
|
1347
1488
|
resolve(nil)
|
|
1348
1489
|
}
|
|
1349
1490
|
|
|
@@ -1352,19 +1493,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1352
1493
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1353
1494
|
reject: @escaping RCTPromiseRejectBlock
|
|
1354
1495
|
) {
|
|
1355
|
-
|
|
1356
|
-
|
|
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)
|
|
1357
1499
|
return
|
|
1358
1500
|
}
|
|
1359
1501
|
Task {
|
|
1360
1502
|
do {
|
|
1361
1503
|
try await client.start()
|
|
1362
1504
|
DispatchQueue.main.async {
|
|
1363
|
-
resolve(
|
|
1505
|
+
settlement.resolve(.void)
|
|
1364
1506
|
}
|
|
1365
1507
|
} catch {
|
|
1366
1508
|
DispatchQueue.main.async {
|
|
1367
|
-
|
|
1509
|
+
Self.rejectWithError(settlement, error)
|
|
1368
1510
|
}
|
|
1369
1511
|
}
|
|
1370
1512
|
}
|
|
@@ -1375,14 +1517,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1375
1517
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1376
1518
|
reject: @escaping RCTPromiseRejectBlock
|
|
1377
1519
|
) {
|
|
1378
|
-
|
|
1379
|
-
|
|
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)
|
|
1380
1523
|
return
|
|
1381
1524
|
}
|
|
1382
1525
|
Task {
|
|
1383
1526
|
await client.stop()
|
|
1384
1527
|
DispatchQueue.main.async {
|
|
1385
|
-
resolve(
|
|
1528
|
+
settlement.resolve(.void)
|
|
1386
1529
|
}
|
|
1387
1530
|
}
|
|
1388
1531
|
}
|
|
@@ -1392,14 +1535,15 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1392
1535
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1393
1536
|
reject: @escaping RCTPromiseRejectBlock
|
|
1394
1537
|
) {
|
|
1395
|
-
|
|
1396
|
-
|
|
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)
|
|
1397
1541
|
return
|
|
1398
1542
|
}
|
|
1399
1543
|
Task {
|
|
1400
1544
|
await client.enterBackground()
|
|
1401
1545
|
DispatchQueue.main.async {
|
|
1402
|
-
resolve(
|
|
1546
|
+
settlement.resolve(.void)
|
|
1403
1547
|
}
|
|
1404
1548
|
}
|
|
1405
1549
|
}
|
|
@@ -1409,19 +1553,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1409
1553
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1410
1554
|
reject: @escaping RCTPromiseRejectBlock
|
|
1411
1555
|
) {
|
|
1412
|
-
|
|
1413
|
-
|
|
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)
|
|
1414
1559
|
return
|
|
1415
1560
|
}
|
|
1416
1561
|
Task {
|
|
1417
1562
|
do {
|
|
1418
1563
|
try await client.enterForeground()
|
|
1419
1564
|
DispatchQueue.main.async {
|
|
1420
|
-
resolve(
|
|
1565
|
+
settlement.resolve(.void)
|
|
1421
1566
|
}
|
|
1422
1567
|
} catch {
|
|
1423
1568
|
DispatchQueue.main.async {
|
|
1424
|
-
|
|
1569
|
+
Self.rejectWithError(settlement, error)
|
|
1425
1570
|
}
|
|
1426
1571
|
}
|
|
1427
1572
|
}
|
|
@@ -1432,19 +1577,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1432
1577
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1433
1578
|
reject: @escaping RCTPromiseRejectBlock
|
|
1434
1579
|
) {
|
|
1435
|
-
|
|
1436
|
-
|
|
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)
|
|
1437
1583
|
return
|
|
1438
1584
|
}
|
|
1439
1585
|
Task {
|
|
1440
1586
|
do {
|
|
1441
1587
|
try await client.retryAfterError()
|
|
1442
1588
|
DispatchQueue.main.async {
|
|
1443
|
-
resolve(
|
|
1589
|
+
settlement.resolve(.void)
|
|
1444
1590
|
}
|
|
1445
1591
|
} catch {
|
|
1446
1592
|
DispatchQueue.main.async {
|
|
1447
|
-
|
|
1593
|
+
Self.rejectWithError(settlement, error)
|
|
1448
1594
|
}
|
|
1449
1595
|
}
|
|
1450
1596
|
}
|
|
@@ -1455,19 +1601,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1455
1601
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1456
1602
|
reject: @escaping RCTPromiseRejectBlock
|
|
1457
1603
|
) {
|
|
1458
|
-
|
|
1459
|
-
|
|
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)
|
|
1460
1607
|
return
|
|
1461
1608
|
}
|
|
1462
1609
|
Task {
|
|
1463
1610
|
do {
|
|
1464
1611
|
try await client.resetSchemaAndStart()
|
|
1465
1612
|
DispatchQueue.main.async {
|
|
1466
|
-
resolve(
|
|
1613
|
+
settlement.resolve(.void)
|
|
1467
1614
|
}
|
|
1468
1615
|
} catch {
|
|
1469
1616
|
DispatchQueue.main.async {
|
|
1470
|
-
|
|
1617
|
+
Self.rejectWithError(settlement, error)
|
|
1471
1618
|
}
|
|
1472
1619
|
}
|
|
1473
1620
|
}
|
|
@@ -1478,19 +1625,20 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1478
1625
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1479
1626
|
reject: @escaping RCTPromiseRejectBlock
|
|
1480
1627
|
) {
|
|
1481
|
-
|
|
1482
|
-
|
|
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)
|
|
1483
1631
|
return
|
|
1484
1632
|
}
|
|
1485
1633
|
Task {
|
|
1486
1634
|
do {
|
|
1487
1635
|
try await client.syncNow()
|
|
1488
1636
|
DispatchQueue.main.async {
|
|
1489
|
-
resolve(
|
|
1637
|
+
settlement.resolve(.void)
|
|
1490
1638
|
}
|
|
1491
1639
|
} catch {
|
|
1492
1640
|
DispatchQueue.main.async {
|
|
1493
|
-
|
|
1641
|
+
Self.rejectWithError(settlement, error)
|
|
1494
1642
|
}
|
|
1495
1643
|
}
|
|
1496
1644
|
}
|
|
@@ -1501,15 +1649,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1501
1649
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1502
1650
|
reject: @escaping RCTPromiseRejectBlock
|
|
1503
1651
|
) {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
rejectWithError(reject, error)
|
|
1512
|
-
}
|
|
1652
|
+
submitDatabaseOperation(
|
|
1653
|
+
resolve: resolve,
|
|
1654
|
+
reject: reject,
|
|
1655
|
+
operation: { client in
|
|
1656
|
+
.value(.integer(Int64(try client.pendingChangeCount())))
|
|
1657
|
+
}
|
|
1658
|
+
)
|
|
1513
1659
|
}
|
|
1514
1660
|
|
|
1515
1661
|
@objc
|
|
@@ -1517,15 +1663,18 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1517
1663
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1518
1664
|
reject: @escaping RCTPromiseRejectBlock
|
|
1519
1665
|
) {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1666
|
+
let runtime = runtime
|
|
1667
|
+
submitDatabaseOperation(
|
|
1668
|
+
resolve: resolve,
|
|
1669
|
+
reject: reject,
|
|
1670
|
+
operation: { client in
|
|
1671
|
+
.value(.string(try Self.encodeBridgeJSON(Self.statusPayload(
|
|
1672
|
+
client.getSyncStatus(),
|
|
1673
|
+
client: client,
|
|
1674
|
+
runtime: runtime
|
|
1675
|
+
))))
|
|
1676
|
+
}
|
|
1677
|
+
)
|
|
1529
1678
|
}
|
|
1530
1679
|
|
|
1531
1680
|
@objc
|
|
@@ -1533,16 +1682,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1533
1682
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1534
1683
|
reject: @escaping RCTPromiseRejectBlock
|
|
1535
1684
|
) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
rejectWithError(reject, error)
|
|
1545
|
-
}
|
|
1685
|
+
submitDatabaseOperation(
|
|
1686
|
+
resolve: resolve,
|
|
1687
|
+
reject: reject,
|
|
1688
|
+
operation: { client in
|
|
1689
|
+
let payload = try client.inspectPendingMutations().map(Self.pendingMutationPayload)
|
|
1690
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1691
|
+
}
|
|
1692
|
+
)
|
|
1546
1693
|
}
|
|
1547
1694
|
|
|
1548
1695
|
@objc
|
|
@@ -1550,16 +1697,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1550
1697
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1551
1698
|
reject: @escaping RCTPromiseRejectBlock
|
|
1552
1699
|
) {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
rejectWithError(reject, error)
|
|
1562
|
-
}
|
|
1700
|
+
submitDatabaseOperation(
|
|
1701
|
+
resolve: resolve,
|
|
1702
|
+
reject: reject,
|
|
1703
|
+
operation: { client in
|
|
1704
|
+
let payload = try client.inspectRetainedMutations().map(Self.pendingMutationPayload)
|
|
1705
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1706
|
+
}
|
|
1707
|
+
)
|
|
1563
1708
|
}
|
|
1564
1709
|
|
|
1565
1710
|
@objc
|
|
@@ -1567,16 +1712,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1567
1712
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1568
1713
|
reject: @escaping RCTPromiseRejectBlock
|
|
1569
1714
|
) {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
rejectWithError(reject, error)
|
|
1579
|
-
}
|
|
1715
|
+
submitDatabaseOperation(
|
|
1716
|
+
resolve: resolve,
|
|
1717
|
+
reject: reject,
|
|
1718
|
+
operation: { client in
|
|
1719
|
+
let payload = try client.inspectRetainedMutationRecords().map(Self.retainedMutationPayload)
|
|
1720
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1721
|
+
}
|
|
1722
|
+
)
|
|
1580
1723
|
}
|
|
1581
1724
|
|
|
1582
1725
|
@objc
|
|
@@ -1584,16 +1727,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1584
1727
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1585
1728
|
reject: @escaping RCTPromiseRejectBlock
|
|
1586
1729
|
) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
rejectWithError(reject, error)
|
|
1596
|
-
}
|
|
1730
|
+
submitDatabaseOperation(
|
|
1731
|
+
resolve: resolve,
|
|
1732
|
+
reject: reject,
|
|
1733
|
+
operation: { client in
|
|
1734
|
+
let payload = try client.inspectRejectedMutations().map(Self.rejectedMutationPayload)
|
|
1735
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1736
|
+
}
|
|
1737
|
+
)
|
|
1597
1738
|
}
|
|
1598
1739
|
|
|
1599
1740
|
@objc
|
|
@@ -1601,16 +1742,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1601
1742
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1602
1743
|
reject: @escaping RCTPromiseRejectBlock
|
|
1603
1744
|
) {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
rejectWithError(reject, error)
|
|
1613
|
-
}
|
|
1745
|
+
submitDatabaseOperation(
|
|
1746
|
+
resolve: resolve,
|
|
1747
|
+
reject: reject,
|
|
1748
|
+
operation: { client in
|
|
1749
|
+
let payload = try client.inspectRejectedMutationRecords().map(Self.retainedRejectionPayload)
|
|
1750
|
+
return .value(.string(try Self.encodeBridgeJSON(payload)))
|
|
1751
|
+
}
|
|
1752
|
+
)
|
|
1614
1753
|
}
|
|
1615
1754
|
|
|
1616
1755
|
/// Reads client state, retained details, and the requested application rows
|
|
@@ -1621,38 +1760,37 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1621
1760
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1622
1761
|
reject: @escaping RCTPromiseRejectBlock
|
|
1623
1762
|
) {
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1763
|
+
let capturedStatements = Result { try Self.captureBridgeStatements(rowStatements) }
|
|
1764
|
+
submitDatabaseOperation(
|
|
1765
|
+
resolve: resolve,
|
|
1766
|
+
reject: reject,
|
|
1767
|
+
operation: { client in
|
|
1768
|
+
let statements = try Self.bridgeStatements(capturedStatements.get())
|
|
1769
|
+
var rows: [Row] = []
|
|
1770
|
+
let snapshot = try SynchroInspection(client: client).captureSnapshot(maximumRecords: 512) { _, transaction in
|
|
1771
|
+
for statement in statements {
|
|
1772
|
+
rows += try transaction.query(statement.sql, params: statement.params)
|
|
1773
|
+
}
|
|
1634
1774
|
}
|
|
1775
|
+
let inspection: [String: Any] = [
|
|
1776
|
+
"client_state": Self.clientStatePayload(snapshot.capture),
|
|
1777
|
+
"migration_journal": try snapshot.capture.migrationJournal.map {
|
|
1778
|
+
try JSONSerialization.jsonObject(with: JSONEncoder().encode($0))
|
|
1779
|
+
} ?? NSNull(),
|
|
1780
|
+
"migration_journal_truncated": snapshot.capture.migrationJournalTruncated,
|
|
1781
|
+
"physical_schema": try JSONSerialization.jsonObject(with: JSONEncoder().encode(snapshot.capture.physicalSchema)),
|
|
1782
|
+
"physical_schema_truncated": snapshot.capture.physicalSchemaTruncated,
|
|
1783
|
+
"accepted_mutation_outcomes": snapshot.capture.acceptedMutationOutcomes,
|
|
1784
|
+
"accepted_mutation_outcomes_truncated": snapshot.capture.acceptedMutationOutcomesTruncated,
|
|
1785
|
+
"retained_mutations": snapshot.retainedMutations.map { $0.map(Self.retainedMutationPayload) } ?? NSNull(),
|
|
1786
|
+
"rejected_mutations": snapshot.rejectedMutations.map { $0.map(Self.retainedRejectionPayload) } ?? NSNull(),
|
|
1787
|
+
]
|
|
1788
|
+
return .value(.object([
|
|
1789
|
+
"inspection": .string(try Self.encodeBridgeJSON(inspection)),
|
|
1790
|
+
"applicationRows": Self.rowsToBridgeValue(rows),
|
|
1791
|
+
]))
|
|
1635
1792
|
}
|
|
1636
|
-
|
|
1637
|
-
"client_state": clientStatePayload(snapshot.capture),
|
|
1638
|
-
"migration_journal": try snapshot.capture.migrationJournal.map {
|
|
1639
|
-
try JSONSerialization.jsonObject(with: JSONEncoder().encode($0))
|
|
1640
|
-
} ?? NSNull(),
|
|
1641
|
-
"migration_journal_truncated": snapshot.capture.migrationJournalTruncated,
|
|
1642
|
-
"physical_schema": try JSONSerialization.jsonObject(with: JSONEncoder().encode(snapshot.capture.physicalSchema)),
|
|
1643
|
-
"physical_schema_truncated": snapshot.capture.physicalSchemaTruncated,
|
|
1644
|
-
"accepted_mutation_outcomes": snapshot.capture.acceptedMutationOutcomes,
|
|
1645
|
-
"accepted_mutation_outcomes_truncated": snapshot.capture.acceptedMutationOutcomesTruncated,
|
|
1646
|
-
"retained_mutations": snapshot.retainedMutations.map { $0.map(retainedMutationPayload) } ?? NSNull(),
|
|
1647
|
-
"rejected_mutations": snapshot.rejectedMutations.map { $0.map(retainedRejectionPayload) } ?? NSNull(),
|
|
1648
|
-
]
|
|
1649
|
-
resolve([
|
|
1650
|
-
"inspection": try encodeBridgeJSON(inspection),
|
|
1651
|
-
"applicationRows": rowsToBridgeRows(rows),
|
|
1652
|
-
])
|
|
1653
|
-
} catch {
|
|
1654
|
-
rejectWithError(reject, error)
|
|
1655
|
-
}
|
|
1793
|
+
)
|
|
1656
1794
|
}
|
|
1657
1795
|
|
|
1658
1796
|
/// Reads one record's row metadata and every rebuild receipt. The inspection snapshot bounds
|
|
@@ -1664,46 +1802,44 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1664
1802
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1665
1803
|
reject: @escaping RCTPromiseRejectBlock
|
|
1666
1804
|
) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1805
|
+
submitDatabaseOperation(
|
|
1806
|
+
resolve: resolve,
|
|
1807
|
+
reject: reject,
|
|
1808
|
+
operation: { client in
|
|
1809
|
+
let inspection = SynchroInspection(client: client)
|
|
1810
|
+
let metadata: Any = try inspection.rowMetadata(
|
|
1811
|
+
tableName: tableName,
|
|
1812
|
+
recordID: recordID
|
|
1813
|
+
).map { value in
|
|
1814
|
+
[
|
|
1815
|
+
"table_name": value.tableName,
|
|
1816
|
+
"record_id": value.recordID,
|
|
1817
|
+
"server_version": value.serverVersion,
|
|
1818
|
+
"row_checksum": value.rowChecksum ?? NSNull(),
|
|
1819
|
+
] as [String: Any]
|
|
1820
|
+
} ?? NSNull()
|
|
1821
|
+
let receipts = try inspection.rebuildReceipts().map { value in
|
|
1822
|
+
[
|
|
1823
|
+
"rebuild_id_fingerprint": value.rebuildIDFingerprint,
|
|
1824
|
+
"page_count": value.pageCount,
|
|
1825
|
+
"returned_record_count": value.returnedRecordCount,
|
|
1826
|
+
"request_chain_expected": value.requestChainExpected,
|
|
1827
|
+
"request_chain_observed": value.requestChainObserved,
|
|
1828
|
+
"record_identities_hex": value.recordIdentitiesHex,
|
|
1829
|
+
"received_row_checksums": value.receivedRowChecksums,
|
|
1830
|
+
"computed_row_checksums": value.computedRowChecksums,
|
|
1831
|
+
"computed_scope_checksum": value.computedScopeChecksum ?? NSNull(),
|
|
1832
|
+
"final_scope_checksum": value.finalScopeChecksum ?? NSNull(),
|
|
1833
|
+
"stored_scope_checksum": value.storedScopeChecksum ?? NSNull(),
|
|
1834
|
+
"local_scope_checksum": value.localScopeChecksum ?? NSNull(),
|
|
1835
|
+
] as [String: Any]
|
|
1836
|
+
}
|
|
1837
|
+
return .value(.string(try Self.encodeBridgeJSON([
|
|
1838
|
+
"row_metadata": metadata,
|
|
1839
|
+
"rebuild_receipts": receipts,
|
|
1840
|
+
])))
|
|
1699
1841
|
}
|
|
1700
|
-
|
|
1701
|
-
"row_metadata": metadata,
|
|
1702
|
-
"rebuild_receipts": receipts,
|
|
1703
|
-
]))
|
|
1704
|
-
} catch {
|
|
1705
|
-
rejectWithError(reject, error)
|
|
1706
|
-
}
|
|
1842
|
+
)
|
|
1707
1843
|
}
|
|
1708
1844
|
|
|
1709
1845
|
@objc
|
|
@@ -1711,7 +1847,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1711
1847
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1712
1848
|
reject: @escaping RCTPromiseRejectBlock
|
|
1713
1849
|
) {
|
|
1714
|
-
guard let transportObservations else {
|
|
1850
|
+
guard let transportObservations = runtime.withLock({ $0.transportObservations }) else {
|
|
1715
1851
|
reject("NOT_CONNECTED", "Transport observation is not configured", nil)
|
|
1716
1852
|
return
|
|
1717
1853
|
}
|
|
@@ -1732,7 +1868,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1732
1868
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1733
1869
|
reject: @escaping RCTPromiseRejectBlock
|
|
1734
1870
|
) {
|
|
1735
|
-
guard let collector = transportObservations else {
|
|
1871
|
+
guard let collector = runtime.withLock({ $0.transportObservations }) else {
|
|
1736
1872
|
reject("INVALID_CONFIG", "Transport pause operation is invalid", nil)
|
|
1737
1873
|
return
|
|
1738
1874
|
}
|
|
@@ -1757,9 +1893,10 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1757
1893
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
1758
1894
|
reject: @escaping RCTPromiseRejectBlock
|
|
1759
1895
|
) {
|
|
1760
|
-
|
|
1896
|
+
let settlement = ReactPromiseSettlement(resolve: resolve, reject: reject)
|
|
1897
|
+
guard let collector = runtime.withLock({ $0.transportObservations }),
|
|
1761
1898
|
timeoutMs.isFinite, timeoutMs >= 1, timeoutMs <= 60_000 else {
|
|
1762
|
-
reject("INVALID_CONFIG", "Transport pause wait is invalid", nil)
|
|
1899
|
+
settlement.reject("INVALID_CONFIG", "Transport pause wait is invalid", nil)
|
|
1763
1900
|
return
|
|
1764
1901
|
}
|
|
1765
1902
|
Task {
|
|
@@ -1771,9 +1908,13 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1771
1908
|
} else {
|
|
1772
1909
|
throw SynchroError.invalidResponse(message: "pause target is invalid")
|
|
1773
1910
|
}
|
|
1774
|
-
DispatchQueue.main.async {
|
|
1911
|
+
DispatchQueue.main.async {
|
|
1912
|
+
settlement.resolve(.void)
|
|
1913
|
+
}
|
|
1775
1914
|
} catch {
|
|
1776
|
-
DispatchQueue.main.async {
|
|
1915
|
+
DispatchQueue.main.async {
|
|
1916
|
+
Self.rejectWithError(settlement, error)
|
|
1917
|
+
}
|
|
1777
1918
|
}
|
|
1778
1919
|
}
|
|
1779
1920
|
}
|
|
@@ -1783,7 +1924,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1783
1924
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1784
1925
|
reject: @escaping RCTPromiseRejectBlock
|
|
1785
1926
|
) {
|
|
1786
|
-
guard let collector = transportObservations else {
|
|
1927
|
+
guard let collector = runtime.withLock({ $0.transportObservations }) else {
|
|
1787
1928
|
reject("NOT_CONNECTED", "Transport observation is not configured", nil)
|
|
1788
1929
|
return
|
|
1789
1930
|
}
|
|
@@ -1808,21 +1949,19 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1808
1949
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1809
1950
|
reject: @escaping RCTPromiseRejectBlock
|
|
1810
1951
|
) {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
rejectWithError(reject, error)
|
|
1820
|
-
}
|
|
1952
|
+
submitDatabaseOperation(
|
|
1953
|
+
resolve: resolve,
|
|
1954
|
+
reject: reject,
|
|
1955
|
+
operation: { client in
|
|
1956
|
+
try client.clearRejectedMutations()
|
|
1957
|
+
return .void
|
|
1958
|
+
}
|
|
1959
|
+
)
|
|
1821
1960
|
}
|
|
1822
1961
|
|
|
1823
1962
|
// MARK: - Helpers
|
|
1824
1963
|
|
|
1825
|
-
private func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
|
|
1964
|
+
private static func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
|
|
1826
1965
|
let schema: Any = capture.schema.map { value in
|
|
1827
1966
|
["version": value.version, "hash": value.hash]
|
|
1828
1967
|
} ?? NSNull()
|
|
@@ -1884,17 +2023,119 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1884
2023
|
]
|
|
1885
2024
|
}
|
|
1886
2025
|
|
|
1887
|
-
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] {
|
|
1888
2118
|
try statements.enumerated().map { index, item in
|
|
1889
2119
|
guard let sql = item["sql"] as? String else {
|
|
1890
2120
|
throw SynchroError.invalidResponse(message: "Missing SQL at index \(index)")
|
|
1891
2121
|
}
|
|
1892
|
-
let params = try (item["params"] as?
|
|
1893
|
-
return
|
|
2122
|
+
let params = try (item["params"] as? NSArray).map(captureSQLParams)
|
|
2123
|
+
return CapturedBridgeStatement(sql: sql, params: params)
|
|
1894
2124
|
}
|
|
1895
2125
|
}
|
|
1896
2126
|
|
|
1897
|
-
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 {
|
|
1898
2139
|
guard JSONSerialization.isValidJSONObject(value) else {
|
|
1899
2140
|
throw SynchroError.invalidResponse(message: "bridge JSON value is invalid")
|
|
1900
2141
|
}
|
|
@@ -1905,7 +2146,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1905
2146
|
return json
|
|
1906
2147
|
}
|
|
1907
2148
|
|
|
1908
|
-
private func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
|
|
2149
|
+
private static func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
|
|
1909
2150
|
switch value {
|
|
1910
2151
|
case .current(let mutation):
|
|
1911
2152
|
return pendingMutationPayload(mutation).merging(["representation": "current"]) { current, _ in current }
|
|
@@ -1925,7 +2166,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1925
2166
|
}
|
|
1926
2167
|
}
|
|
1927
2168
|
|
|
1928
|
-
private func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
|
|
2169
|
+
private static func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
|
|
1929
2170
|
[
|
|
1930
2171
|
"mutationID": mutation.mutationID,
|
|
1931
2172
|
"localOrder": mutation.localOrder,
|
|
@@ -1957,7 +2198,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1957
2198
|
]
|
|
1958
2199
|
}
|
|
1959
2200
|
|
|
1960
|
-
private func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
|
|
2201
|
+
private static func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
|
|
1961
2202
|
switch value {
|
|
1962
2203
|
case .current(let rejection):
|
|
1963
2204
|
return rejectedMutationPayload(rejection).merging(["representation": "current"]) { current, _ in current }
|
|
@@ -1978,7 +2219,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
1978
2219
|
}
|
|
1979
2220
|
}
|
|
1980
2221
|
|
|
1981
|
-
private func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
|
|
2222
|
+
private static func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
|
|
1982
2223
|
[
|
|
1983
2224
|
"mutationID": mutation.mutationID,
|
|
1984
2225
|
"tableName": mutation.tableName,
|
|
@@ -2015,14 +2256,14 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2015
2256
|
private static let maxSafeInteger: Int64 = 9_007_199_254_740_991
|
|
2016
2257
|
private static let minSafeInteger: Int64 = -9_007_199_254_740_991
|
|
2017
2258
|
|
|
2018
|
-
private func canonicalBase64URL(_ data: Data) -> String {
|
|
2259
|
+
private static func canonicalBase64URL(_ data: Data) -> String {
|
|
2019
2260
|
data.base64EncodedString()
|
|
2020
2261
|
.replacingOccurrences(of: "+", with: "-")
|
|
2021
2262
|
.replacingOccurrences(of: "/", with: "_")
|
|
2022
2263
|
.replacingOccurrences(of: "=", with: "")
|
|
2023
2264
|
}
|
|
2024
2265
|
|
|
2025
|
-
private func decodeCanonicalBase64URL(_ value: String, index: Int) throws -> Data {
|
|
2266
|
+
private static func decodeCanonicalBase64URL(_ value: String, index: Int) throws -> Data {
|
|
2026
2267
|
guard value.utf8.allSatisfy({ byte in
|
|
2027
2268
|
(byte >= 65 && byte <= 90) ||
|
|
2028
2269
|
(byte >= 97 && byte <= 122) ||
|
|
@@ -2047,7 +2288,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2047
2288
|
return data
|
|
2048
2289
|
}
|
|
2049
2290
|
|
|
2050
|
-
private func decodeCanonicalInt64(_ value: String, index: Int) throws -> Int64 {
|
|
2291
|
+
private static func decodeCanonicalInt64(_ value: String, index: Int) throws -> Int64 {
|
|
2051
2292
|
guard value == "0" || value.range(of: "^-?[1-9][0-9]*$", options: .regularExpression) != nil,
|
|
2052
2293
|
let parsed = Int64(value), String(parsed) == value else {
|
|
2053
2294
|
throw BridgeParamError.invalidTag(index: index, reason: "int64 is not canonical or is out of range")
|
|
@@ -2055,46 +2296,53 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2055
2296
|
return parsed
|
|
2056
2297
|
}
|
|
2057
2298
|
|
|
2058
|
-
private func bridgeTagToParam(
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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 {
|
|
2062
2306
|
throw BridgeParamError.invalidTag(index: index, reason: "tag must contain exactly type and payload fields")
|
|
2063
2307
|
}
|
|
2064
2308
|
|
|
2065
2309
|
switch type {
|
|
2066
2310
|
case "bytes":
|
|
2067
|
-
guard let base64 =
|
|
2311
|
+
guard let base64 = stringField("base64"), !containsField("value") else {
|
|
2068
2312
|
throw BridgeParamError.invalidTag(index: index, reason: "bytes tag must contain base64")
|
|
2069
2313
|
}
|
|
2070
|
-
return
|
|
2314
|
+
return (
|
|
2315
|
+
["type": .string(type), "base64": .string(base64)],
|
|
2316
|
+
try decodeCanonicalBase64URL(base64, index: index)
|
|
2317
|
+
)
|
|
2071
2318
|
case "int64":
|
|
2072
|
-
guard let value =
|
|
2319
|
+
guard let value = stringField("value"), !containsField("base64") else {
|
|
2073
2320
|
throw BridgeParamError.invalidTag(index: index, reason: "int64 tag must contain value")
|
|
2074
2321
|
}
|
|
2075
|
-
return
|
|
2322
|
+
return (
|
|
2323
|
+
["type": .string(type), "value": .string(value)],
|
|
2324
|
+
try decodeCanonicalInt64(value, index: index)
|
|
2325
|
+
)
|
|
2076
2326
|
default:
|
|
2077
2327
|
throw BridgeParamError.invalidTag(index: index, reason: "unknown tag type")
|
|
2078
2328
|
}
|
|
2079
2329
|
}
|
|
2080
2330
|
|
|
2081
|
-
private func bridgeValueToParam(
|
|
2331
|
+
private static func bridgeValueToParam(
|
|
2332
|
+
_ value: BridgeValue,
|
|
2333
|
+
index: Int
|
|
2334
|
+
) throws -> (any DatabaseValueConvertible)? {
|
|
2082
2335
|
switch value {
|
|
2083
|
-
case
|
|
2336
|
+
case .null:
|
|
2084
2337
|
return nil
|
|
2085
|
-
case let boolVal
|
|
2338
|
+
case .bool(let boolVal):
|
|
2086
2339
|
return boolVal ? 1 : 0
|
|
2087
|
-
case let intVal
|
|
2088
|
-
guard
|
|
2340
|
+
case .integer(let intVal):
|
|
2341
|
+
guard intVal >= Self.minSafeInteger && intVal <= Self.maxSafeInteger else {
|
|
2089
2342
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2090
2343
|
}
|
|
2091
2344
|
return intVal
|
|
2092
|
-
case let
|
|
2093
|
-
guard int64Val >= Self.minSafeInteger && int64Val <= Self.maxSafeInteger else {
|
|
2094
|
-
throw BridgeParamError.invalidNumber(index: index)
|
|
2095
|
-
}
|
|
2096
|
-
return int64Val
|
|
2097
|
-
case let doubleVal as Double:
|
|
2345
|
+
case .double(let doubleVal):
|
|
2098
2346
|
guard doubleVal.isFinite else {
|
|
2099
2347
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2100
2348
|
}
|
|
@@ -2103,40 +2351,32 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2103
2351
|
throw BridgeParamError.invalidNumber(index: index)
|
|
2104
2352
|
}
|
|
2105
2353
|
return doubleVal
|
|
2106
|
-
case let stringVal
|
|
2354
|
+
case .string(let stringVal):
|
|
2107
2355
|
return stringVal
|
|
2108
|
-
case let dictionary
|
|
2109
|
-
return try bridgeTagToParam(
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
return doubleValue
|
|
2128
|
-
default:
|
|
2129
|
-
throw BridgeParamError.unsupported(index: index, type: String(describing: type(of: value)))
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
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)?] {
|
|
2134
2374
|
try values.enumerated().map { index, value in
|
|
2135
2375
|
try bridgeValueToParam(value, index: index)
|
|
2136
2376
|
}
|
|
2137
2377
|
}
|
|
2138
2378
|
|
|
2139
|
-
private func parseColumns(_ json: String) throws -> [ColumnDef] {
|
|
2379
|
+
private static func parseColumns(_ json: String) throws -> [ColumnDef] {
|
|
2140
2380
|
struct Column: Decodable {
|
|
2141
2381
|
let name: String
|
|
2142
2382
|
let type: String
|
|
@@ -2165,7 +2405,7 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2165
2405
|
}
|
|
2166
2406
|
}
|
|
2167
2407
|
|
|
2168
|
-
private func parseTableOptions(_ json: String) throws -> Synchro.TableOptions {
|
|
2408
|
+
private static func parseTableOptions(_ json: String) throws -> Synchro.TableOptions {
|
|
2169
2409
|
struct Options: Decodable {
|
|
2170
2410
|
let ifNotExists: Bool?
|
|
2171
2411
|
let withoutRowid: Bool?
|
|
@@ -2186,34 +2426,34 @@ public class SynchroModuleImpl: NSObject {
|
|
|
2186
2426
|
)
|
|
2187
2427
|
}
|
|
2188
2428
|
|
|
2189
|
-
private func
|
|
2429
|
+
private static func databaseValueToBridgeValue(_ dbValue: DatabaseValue) -> BridgeValue {
|
|
2190
2430
|
switch dbValue.storage {
|
|
2191
2431
|
case .null:
|
|
2192
|
-
return
|
|
2432
|
+
return .null
|
|
2193
2433
|
case .int64(let v):
|
|
2194
2434
|
if v >= Self.minSafeInteger && v <= Self.maxSafeInteger {
|
|
2195
|
-
return
|
|
2435
|
+
return .integer(v)
|
|
2196
2436
|
}
|
|
2197
|
-
return ["type": "int64", "value": String(v)]
|
|
2437
|
+
return .object(["type": .string("int64"), "value": .string(String(v))])
|
|
2198
2438
|
case .double(let v):
|
|
2199
|
-
return
|
|
2439
|
+
return .double(v)
|
|
2200
2440
|
case .string(let v):
|
|
2201
|
-
return v
|
|
2441
|
+
return .string(v)
|
|
2202
2442
|
case .blob(let v):
|
|
2203
|
-
return ["type": "bytes", "base64": canonicalBase64URL(v)]
|
|
2443
|
+
return .object(["type": .string("bytes"), "base64": .string(canonicalBase64URL(v))])
|
|
2204
2444
|
}
|
|
2205
2445
|
}
|
|
2206
2446
|
|
|
2207
|
-
private func
|
|
2208
|
-
var
|
|
2447
|
+
private static func rowToBridgeValue(_ row: Row) -> BridgeValue {
|
|
2448
|
+
var values: [String: BridgeValue] = [:]
|
|
2209
2449
|
for column in row.columnNames {
|
|
2210
2450
|
let dbValue: DatabaseValue = row[column]
|
|
2211
|
-
|
|
2451
|
+
values[column] = databaseValueToBridgeValue(dbValue)
|
|
2212
2452
|
}
|
|
2213
|
-
return
|
|
2453
|
+
return .object(values)
|
|
2214
2454
|
}
|
|
2215
2455
|
|
|
2216
|
-
private func
|
|
2217
|
-
rows.map
|
|
2456
|
+
private static func rowsToBridgeValue(_ rows: [Row]) -> BridgeValue {
|
|
2457
|
+
.array(rows.map(rowToBridgeValue))
|
|
2218
2458
|
}
|
|
2219
2459
|
}
|