@trainstar/synchro-react-native 0.4.0-rc.3 → 0.4.0-rc.4

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