@trainstar/synchro-react-native 0.3.1 → 0.4.0-rc.1

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.
Files changed (45) hide show
  1. package/README.md +58 -4
  2. package/SynchroReactNative.podspec +6 -0
  3. package/android/build.gradle +5 -1
  4. package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/SynchroModuleTransactionTest.kt +591 -0
  5. package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/TransactionSessionTest.kt +314 -0
  6. package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroModule.kt +307 -110
  7. package/ios/SynchroModule.mm +27 -4
  8. package/ios/SynchroModule.swift +228 -84
  9. package/ios-tests/SynchroModuleTransactionTests.swift +707 -0
  10. package/ios-tests/TransactionSessionTests.swift +66 -0
  11. package/lib/module/NativeSynchro.js.map +1 -1
  12. package/lib/module/SynchroClient.js +116 -3
  13. package/lib/module/SynchroClient.js.map +1 -1
  14. package/lib/module/errors.js +15 -1
  15. package/lib/module/errors.js.map +1 -1
  16. package/lib/module/index.js +1 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/inspection.js +31 -3
  19. package/lib/module/inspection.js.map +1 -1
  20. package/lib/module/types.js +18 -0
  21. package/lib/module/types.js.map +1 -1
  22. package/lib/typescript/example/e2e/corpus-harness.d.ts.map +1 -1
  23. package/lib/typescript/example/src/conformance/runner.d.ts +1 -0
  24. package/lib/typescript/example/src/conformance/runner.d.ts.map +1 -1
  25. package/lib/typescript/example/src/timeouts.d.ts +2 -0
  26. package/lib/typescript/example/src/timeouts.d.ts.map +1 -0
  27. package/lib/typescript/src/NativeSynchro.d.ts +7 -1
  28. package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
  29. package/lib/typescript/src/SynchroClient.d.ts +29 -1
  30. package/lib/typescript/src/SynchroClient.d.ts.map +1 -1
  31. package/lib/typescript/src/errors.d.ts +7 -1
  32. package/lib/typescript/src/errors.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +3 -3
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/lib/typescript/src/inspection.d.ts +13 -2
  36. package/lib/typescript/src/inspection.d.ts.map +1 -1
  37. package/lib/typescript/src/types.d.ts +45 -2
  38. package/lib/typescript/src/types.d.ts.map +1 -1
  39. package/package.json +3 -2
  40. package/src/NativeSynchro.ts +6 -1
  41. package/src/SynchroClient.ts +129 -1
  42. package/src/errors.ts +30 -0
  43. package/src/index.ts +7 -0
  44. package/src/inspection.ts +44 -2
  45. package/src/types.ts +51 -2
@@ -141,6 +141,11 @@
141
141
  [self.impl beginWriteTransaction:resolve reject:reject];
142
142
  }
143
143
 
144
+ - (void)beginAtomicWriteTransaction:(RCTPromiseResolveBlock)resolve
145
+ reject:(RCTPromiseRejectBlock)reject {
146
+ [self.impl beginAtomicWriteTransaction:resolve reject:reject];
147
+ }
148
+
144
149
  - (void)beginReadTransaction:(RCTPromiseResolveBlock)resolve
145
150
  reject:(RCTPromiseRejectBlock)reject {
146
151
  [self.impl beginReadTransaction:resolve reject:reject];
@@ -282,14 +287,25 @@
282
287
  [self.impl inspectRetainedMutations:resolve reject:reject];
283
288
  }
284
289
 
290
+ - (void)inspectRetainedMutationRecords:(RCTPromiseResolveBlock)resolve
291
+ reject:(RCTPromiseRejectBlock)reject {
292
+ [self.impl inspectRetainedMutationRecords:resolve reject:reject];
293
+ }
294
+
285
295
  - (void)inspectRejectedMutations:(RCTPromiseResolveBlock)resolve
286
296
  reject:(RCTPromiseRejectBlock)reject {
287
297
  [self.impl inspectRejectedMutations:resolve reject:reject];
288
298
  }
289
299
 
290
- - (void)inspectClientState:(RCTPromiseResolveBlock)resolve
291
- reject:(RCTPromiseRejectBlock)reject {
292
- [self.impl inspectClientState:resolve reject:reject];
300
+ - (void)inspectRejectedMutationRecords:(RCTPromiseResolveBlock)resolve
301
+ reject:(RCTPromiseRejectBlock)reject {
302
+ [self.impl inspectRejectedMutationRecords:resolve reject:reject];
303
+ }
304
+
305
+ - (void)inspectClientStateSnapshot:(NSArray *)rowStatements
306
+ resolve:(RCTPromiseResolveBlock)resolve
307
+ reject:(RCTPromiseRejectBlock)reject {
308
+ [self.impl inspectClientStateSnapshot:rowStatements resolve:resolve reject:reject];
293
309
  }
294
310
 
295
311
  - (void)inspectDurableState:(NSString *)tableName
@@ -391,6 +407,8 @@ RCT_EXTERN_METHOD(executeBatch:(NSArray *)statements
391
407
  reject:(RCTPromiseRejectBlock)reject)
392
408
  RCT_EXTERN_METHOD(beginWriteTransaction:(RCTPromiseResolveBlock)resolve
393
409
  reject:(RCTPromiseRejectBlock)reject)
410
+ RCT_EXTERN_METHOD(beginAtomicWriteTransaction:(RCTPromiseResolveBlock)resolve
411
+ reject:(RCTPromiseRejectBlock)reject)
394
412
  RCT_EXTERN_METHOD(beginReadTransaction:(RCTPromiseResolveBlock)resolve
395
413
  reject:(RCTPromiseRejectBlock)reject)
396
414
  RCT_EXTERN_METHOD(txQuery:(NSString *)txID
@@ -463,9 +481,14 @@ RCT_EXTERN_METHOD(inspectPendingMutations:(RCTPromiseResolveBlock)resolve
463
481
  reject:(RCTPromiseRejectBlock)reject)
464
482
  RCT_EXTERN_METHOD(inspectRetainedMutations:(RCTPromiseResolveBlock)resolve
465
483
  reject:(RCTPromiseRejectBlock)reject)
484
+ RCT_EXTERN_METHOD(inspectRetainedMutationRecords:(RCTPromiseResolveBlock)resolve
485
+ reject:(RCTPromiseRejectBlock)reject)
466
486
  RCT_EXTERN_METHOD(inspectRejectedMutations:(RCTPromiseResolveBlock)resolve
467
487
  reject:(RCTPromiseRejectBlock)reject)
468
- RCT_EXTERN_METHOD(inspectClientState:(RCTPromiseResolveBlock)resolve
488
+ RCT_EXTERN_METHOD(inspectRejectedMutationRecords:(RCTPromiseResolveBlock)resolve
489
+ reject:(RCTPromiseRejectBlock)reject)
490
+ RCT_EXTERN_METHOD(inspectClientStateSnapshot:(NSArray *)rowStatements
491
+ resolve:(RCTPromiseResolveBlock)resolve
469
492
  reject:(RCTPromiseRejectBlock)reject)
470
493
  RCT_EXTERN_METHOD(inspectDurableState:(NSString *)tableName
471
494
  recordID:(NSString *)recordID
@@ -12,7 +12,7 @@ import GRDB
12
12
 
13
13
  // MARK: - Transaction Session
14
14
 
15
- private class TransactionSession {
15
+ class TransactionSession {
16
16
  private let condition = NSCondition()
17
17
  private var operations: [TransactionOp] = []
18
18
  private var closed = false
@@ -20,10 +20,36 @@ private class TransactionSession {
20
20
  private var finalCompletion: ((Result<Void, Error>) -> Void)?
21
21
  private var finished = false
22
22
  private var finishWaiters: [CheckedContinuation<Void, Never>] = []
23
+ private var beginCompletion: ((Result<String, Error>) -> Void)?
23
24
  let isWrite: Bool
24
25
 
25
- init(isWrite: Bool) {
26
+ init(isWrite: Bool, beginCompletion: @escaping (Result<String, Error>) -> Void) {
26
27
  self.isWrite = isWrite
28
+ self.beginCompletion = beginCompletion
29
+ }
30
+
31
+ /// Settles begin when the native transaction callback starts. A close that
32
+ /// aborted the session before the callback started rejects begin instead.
33
+ 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))
41
+ throw error
42
+ }
43
+ completion?(.success(txID))
44
+ }
45
+
46
+ /// Rejects begin when the transaction fails before its callback starts.
47
+ func rejectBegin(_ error: Error) {
48
+ condition.lock()
49
+ let completion = beginCompletion
50
+ beginCompletion = nil
51
+ condition.unlock()
52
+ completion?(.failure(error))
27
53
  }
28
54
 
29
55
  func enqueue(_ op: TransactionOp) -> Bool {
@@ -132,7 +158,7 @@ private class TransactionSession {
132
158
  }
133
159
  }
134
160
 
135
- private enum TransactionOp {
161
+ enum TransactionOp {
136
162
  case query(sql: String, params: [Any], completion: (Result<[[String: Any]], Error>) -> Void)
137
163
  case queryOne(sql: String, params: [Any], completion: (Result<[String: Any]?, Error>) -> Void)
138
164
  case execute(sql: String, params: [Any], completion: (Result<[String: Any], Error>) -> Void)
@@ -191,10 +217,10 @@ private actor LifecycleMutex {
191
217
  public class SynchroModuleImpl: NSObject {
192
218
  @objc public weak var eventDelegate: SynchroEventEmitting?
193
219
 
194
- private var client: SynchroClient?
220
+ private(set) var client: SynchroClient?
195
221
  private let lifecycleMutex = LifecycleMutex()
196
- private var sessions: [String: TransactionSession] = [:]
197
- private let sessionsLock = NSLock()
222
+ var sessions: [String: TransactionSession] = [:]
223
+ let sessionsLock = NSLock()
198
224
  private var acceptingTransactions = false
199
225
  private var observers: [String: any Synchro.Cancellable] = [:]
200
226
  private var statusSubscription: (any Synchro.Cancellable)?
@@ -277,6 +303,8 @@ public class SynchroModuleImpl: NSObject {
277
303
  return ("ALREADY_STARTED", [:])
278
304
  case .notStarted:
279
305
  return ("NOT_STARTED", [:])
306
+ case .atomicGroupInvalid(let reason):
307
+ return ("atomic_group_invalid", ["reason": reason.rawValue])
280
308
  }
281
309
  }
282
310
 
@@ -557,17 +585,7 @@ public class SynchroModuleImpl: NSObject {
557
585
  return
558
586
  }
559
587
  do {
560
- let nativeStatements: [Synchro.SQLStatement] = try statements.map { item in
561
- let sql = item["sql"] as! String
562
- let params: [(any DatabaseValueConvertible)?]?
563
- if let bridgeValues = item["params"] as? [Any] {
564
- params = try bridgeParams(bridgeValues)
565
- } else {
566
- params = nil
567
- }
568
- return Synchro.SQLStatement(sql: sql, params: params)
569
- }
570
- let total = try client.executeBatch(nativeStatements)
588
+ let total = try client.executeBatch(try bridgeStatements(statements))
571
589
  resolve(["totalRowsAffected": total])
572
590
  } catch {
573
591
  rejectWithError(reject, error)
@@ -584,6 +602,14 @@ public class SynchroModuleImpl: NSObject {
584
602
  beginTransaction(isWrite: true, resolve: resolve, reject: reject)
585
603
  }
586
604
 
605
+ @objc
606
+ public func beginAtomicWriteTransaction(
607
+ _ resolve: @escaping RCTPromiseResolveBlock,
608
+ reject: @escaping RCTPromiseRejectBlock
609
+ ) {
610
+ beginTransaction(isWrite: true, isAtomic: true, resolve: resolve, reject: reject)
611
+ }
612
+
587
613
  @objc
588
614
  public func beginReadTransaction(
589
615
  _ resolve: @escaping RCTPromiseResolveBlock,
@@ -595,12 +621,11 @@ public class SynchroModuleImpl: NSObject {
595
621
  private func runTransactionLoop(
596
622
  session: TransactionSession,
597
623
  txID: String,
598
- resolve: @escaping RCTPromiseResolveBlock,
599
624
  query: (String, [(any DatabaseValueConvertible)?]) throws -> [Row],
600
625
  queryOne: (String, [(any DatabaseValueConvertible)?]) throws -> Row?,
601
626
  execute: ((String, [(any DatabaseValueConvertible)?]) throws -> Int)?
602
627
  ) throws {
603
- resolve(txID)
628
+ try session.acceptBegin(txID)
604
629
 
605
630
  while true {
606
631
  guard let op = try session.nextOperation(timeout: 5) else {
@@ -669,6 +694,7 @@ public class SynchroModuleImpl: NSObject {
669
694
 
670
695
  private func beginTransaction(
671
696
  isWrite: Bool,
697
+ isAtomic: Bool = false,
672
698
  resolve: @escaping RCTPromiseResolveBlock,
673
699
  reject: @escaping RCTPromiseRejectBlock
674
700
  ) {
@@ -680,7 +706,12 @@ public class SynchroModuleImpl: NSObject {
680
706
  }
681
707
 
682
708
  let txID = UUID().uuidString
683
- let session = TransactionSession(isWrite: isWrite)
709
+ let session = TransactionSession(isWrite: isWrite) { result in
710
+ switch result {
711
+ case .success(let acceptedID): resolve(acceptedID)
712
+ case .failure(let error): self.rejectWithError(reject, error)
713
+ }
714
+ }
684
715
  sessions[txID] = session
685
716
  sessionsLock.unlock()
686
717
 
@@ -695,11 +726,10 @@ public class SynchroModuleImpl: NSObject {
695
726
  do {
696
727
  let finalResult: Result<Void, Error>
697
728
  if isWrite {
698
- try client.writeTransaction { transaction in
729
+ let body: (ApplicationTransaction) throws -> Void = { transaction in
699
730
  try self.runTransactionLoop(
700
731
  session: session,
701
732
  txID: txID,
702
- resolve: resolve,
703
733
  query: { sql, params in
704
734
  try transaction.query(sql, params: params)
705
735
  },
@@ -711,12 +741,16 @@ public class SynchroModuleImpl: NSObject {
711
741
  }
712
742
  )
713
743
  }
744
+ if isAtomic {
745
+ try client.atomicWriteTransaction(body)
746
+ } else {
747
+ try client.writeTransaction(body)
748
+ }
714
749
  } else {
715
750
  try client.readTransaction { db in
716
751
  try self.runTransactionLoop(
717
752
  session: session,
718
753
  txID: txID,
719
- resolve: resolve,
720
754
  query: { sql, params in
721
755
  let statement = try db.makeStatement(sql: sql)
722
756
  guard statement.isReadonly else {
@@ -748,6 +782,8 @@ public class SynchroModuleImpl: NSObject {
748
782
  } catch is TransactionRollbackError {
749
783
  session.completeFinal(.success(()))
750
784
  } catch {
785
+ // Acquisition can fail before the callback settles begin.
786
+ session.rejectBegin(error)
751
787
  session.completeFinal(.failure(error))
752
788
  }
753
789
  }
@@ -842,6 +878,7 @@ public class SynchroModuleImpl: NSObject {
842
878
  let op = TransactionOp.commit { result in
843
879
  switch result {
844
880
  case .success: resolve(nil)
881
+ case .failure(let error as SynchroError): self.rejectWithError(reject, error)
845
882
  case .failure(let error): reject("DATABASE_ERROR", error.localizedDescription, error)
846
883
  }
847
884
  }
@@ -1525,6 +1562,23 @@ public class SynchroModuleImpl: NSObject {
1525
1562
  }
1526
1563
  }
1527
1564
 
1565
+ @objc
1566
+ public func inspectRetainedMutationRecords(
1567
+ _ resolve: @escaping RCTPromiseResolveBlock,
1568
+ reject: @escaping RCTPromiseRejectBlock
1569
+ ) {
1570
+ guard let client = client else {
1571
+ reject("NOT_CONNECTED", "Client not initialized", nil)
1572
+ return
1573
+ }
1574
+ do {
1575
+ let payload = try client.inspectRetainedMutationRecords().map(retainedMutationPayload)
1576
+ resolve(try encodeBridgeJSON(payload))
1577
+ } catch {
1578
+ rejectWithError(reject, error)
1579
+ }
1580
+ }
1581
+
1528
1582
  @objc
1529
1583
  public func inspectRejectedMutations(
1530
1584
  _ resolve: @escaping RCTPromiseResolveBlock,
@@ -1543,81 +1597,58 @@ public class SynchroModuleImpl: NSObject {
1543
1597
  }
1544
1598
 
1545
1599
  @objc
1546
- public func inspectClientState(
1600
+ public func inspectRejectedMutationRecords(
1547
1601
  _ resolve: @escaping RCTPromiseResolveBlock,
1548
1602
  reject: @escaping RCTPromiseRejectBlock
1603
+ ) {
1604
+ guard let client = client else {
1605
+ reject("NOT_CONNECTED", "Client not initialized", nil)
1606
+ return
1607
+ }
1608
+ do {
1609
+ let payload = try client.inspectRejectedMutationRecords().map(retainedRejectionPayload)
1610
+ resolve(try encodeBridgeJSON(payload))
1611
+ } catch {
1612
+ rejectWithError(reject, error)
1613
+ }
1614
+ }
1615
+
1616
+ /// Reads client state, retained details, and the requested application rows
1617
+ /// from one read-only snapshot, as the native conformance runners do.
1618
+ @objc
1619
+ public func inspectClientStateSnapshot(
1620
+ _ rowStatements: [[String: Any]],
1621
+ resolve: @escaping RCTPromiseResolveBlock,
1622
+ reject: @escaping RCTPromiseRejectBlock
1549
1623
  ) {
1550
1624
  guard let client else {
1551
1625
  reject("NOT_CONNECTED", "Client not initialized", nil)
1552
1626
  return
1553
1627
  }
1554
1628
  do {
1555
- let inspection = SynchroInspection(client: client)
1556
- let capture = try inspection.captureState(maximumRecords: 512)
1557
- let schema: Any = capture.schema.map { value in
1558
- ["version": value.version, "hash": value.hash]
1559
- } ?? NSNull()
1560
- let scopeStates = capture.scopeStates.map { value in
1561
- [
1562
- "scope_id": value.scopeID,
1563
- "cursor": value.cursor ?? NSNull(),
1564
- "checksum": value.checksum ?? NSNull(),
1565
- "local_checksum": value.localChecksum,
1566
- "generation": value.generation,
1567
- ] as [String: Any]
1568
- }
1569
- let scopeRows = capture.scopeRows.map { value in
1570
- [
1571
- "scope_id": value.scopeID,
1572
- "table_name": value.tableName,
1573
- "record_id": value.recordID,
1574
- "checksum": value.checksum,
1575
- "generation": value.generation,
1576
- ] as [String: Any]
1577
- }
1578
- let attempts = capture.rebuildAttempts.map { value in
1579
- [
1580
- "scope_id": value.scopeID,
1581
- "rebuild_id": value.rebuildID,
1582
- "client_generation": value.clientGeneration,
1583
- "schema_version": value.schemaVersion,
1584
- "schema_hash": value.schemaHash,
1585
- "generation": value.generation,
1586
- "cursor": value.cursor ?? NSNull(),
1587
- "page_limit": value.pageLimit,
1588
- ] as [String: Any]
1629
+ let statements = try bridgeStatements(rowStatements)
1630
+ var rows: [Row] = []
1631
+ let snapshot = try SynchroInspection(client: client).captureSnapshot(maximumRecords: 512) { _, transaction in
1632
+ for statement in statements {
1633
+ rows += try transaction.query(statement.sql, params: statement.params)
1634
+ }
1589
1635
  }
1590
- resolve(try encodeBridgeJSON([
1591
- "schema": schema,
1592
- "scope_states": scopeStates,
1593
- "scope_rows": scopeRows,
1594
- "rebuild_attempts": attempts,
1595
- "application_row_count": capture.applicationRowCount,
1596
- "mutation_ledger_count": capture.mutationLedgerCount,
1597
- "mutation_outcome_count": capture.mutationOutcomeCount,
1598
- "sealed_batch_count": capture.sealedBatchCount,
1599
- "rejected_mutation_count": capture.rejectedMutationCount,
1600
- "scope_state_count": capture.scopeStateCount,
1601
- "scope_row_count": capture.scopeRowCount,
1602
- "provenance_count": capture.provenanceCount,
1603
- "row_metadata_count": capture.rowMetadataCount,
1604
- "rebuild_attempt_count": capture.rebuildAttemptCount,
1605
- "rebuild_receipt_count": capture.rebuildReceiptCount,
1606
- "scope_states_truncated": capture.scopeStatesTruncated,
1607
- "scope_rows_truncated": capture.scopeRowsTruncated,
1608
- "rebuild_attempts_truncated": capture.rebuildAttemptsTruncated,
1609
- "rebuild_receipts_truncated": capture.rebuildReceiptsTruncated,
1610
- "row_metadata_truncated": capture.rowMetadataTruncated,
1611
- "capture_overflowed": capture.overflowed,
1612
- "provenance_maintenance_work_cursor": String(
1613
- capture.provenanceMaintenanceWorkCursor
1614
- ),
1615
- ]))
1636
+ let inspection: [String: Any] = [
1637
+ "client_state": clientStatePayload(snapshot.capture),
1638
+ "retained_mutations": snapshot.retainedMutations.map { $0.map(retainedMutationPayload) } ?? NSNull(),
1639
+ "rejected_mutations": snapshot.rejectedMutations.map { $0.map(retainedRejectionPayload) } ?? NSNull(),
1640
+ ]
1641
+ resolve([
1642
+ "inspection": try encodeBridgeJSON(inspection),
1643
+ "applicationRows": rowsToBridgeRows(rows),
1644
+ ])
1616
1645
  } catch {
1617
1646
  rejectWithError(reject, error)
1618
1647
  }
1619
1648
  }
1620
1649
 
1650
+ /// Reads one record's row metadata and every rebuild receipt. The inspection snapshot bounds
1651
+ /// row metadata, so a durable proof reads its identity directly.
1621
1652
  @objc
1622
1653
  public func inspectDurableState(
1623
1654
  _ tableName: String,
@@ -1773,6 +1804,78 @@ public class SynchroModuleImpl: NSObject {
1773
1804
 
1774
1805
  // MARK: - Helpers
1775
1806
 
1807
+ private func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
1808
+ let schema: Any = capture.schema.map { value in
1809
+ ["version": value.version, "hash": value.hash]
1810
+ } ?? NSNull()
1811
+ let scopeStates = capture.scopeStates.map { value in
1812
+ [
1813
+ "scope_id": value.scopeID,
1814
+ "cursor": value.cursor ?? NSNull(),
1815
+ "checksum": value.checksum ?? NSNull(),
1816
+ "local_checksum": value.localChecksum,
1817
+ "generation": value.generation,
1818
+ ] as [String: Any]
1819
+ }
1820
+ let scopeRows = capture.scopeRows.map { value in
1821
+ [
1822
+ "scope_id": value.scopeID,
1823
+ "table_name": value.tableName,
1824
+ "record_id": value.recordID,
1825
+ "checksum": value.checksum,
1826
+ "generation": value.generation,
1827
+ ] as [String: Any]
1828
+ }
1829
+ let attempts = capture.rebuildAttempts.map { value in
1830
+ [
1831
+ "scope_id": value.scopeID,
1832
+ "rebuild_id": value.rebuildID,
1833
+ "client_generation": value.clientGeneration,
1834
+ "schema_version": value.schemaVersion,
1835
+ "schema_hash": value.schemaHash,
1836
+ "generation": value.generation,
1837
+ "cursor": value.cursor ?? NSNull(),
1838
+ "page_limit": value.pageLimit,
1839
+ ] as [String: Any]
1840
+ }
1841
+ return [
1842
+ "schema": schema,
1843
+ "scope_states": scopeStates,
1844
+ "scope_rows": scopeRows,
1845
+ "rebuild_attempts": attempts,
1846
+ "application_row_count": capture.applicationRowCount,
1847
+ "mutation_ledger_count": capture.mutationLedgerCount,
1848
+ "mutation_outcome_count": capture.mutationOutcomeCount,
1849
+ "sealed_batch_count": capture.sealedBatchCount,
1850
+ "rejected_mutation_count": capture.rejectedMutationCount,
1851
+ "scope_state_count": capture.scopeStateCount,
1852
+ "scope_row_count": capture.scopeRowCount,
1853
+ "provenance_count": capture.provenanceCount,
1854
+ "row_metadata_count": capture.rowMetadataCount,
1855
+ "rebuild_attempt_count": capture.rebuildAttemptCount,
1856
+ "rebuild_receipt_count": capture.rebuildReceiptCount,
1857
+ "scope_states_truncated": capture.scopeStatesTruncated,
1858
+ "scope_rows_truncated": capture.scopeRowsTruncated,
1859
+ "rebuild_attempts_truncated": capture.rebuildAttemptsTruncated,
1860
+ "rebuild_receipts_truncated": capture.rebuildReceiptsTruncated,
1861
+ "row_metadata_truncated": capture.rowMetadataTruncated,
1862
+ "capture_overflowed": capture.overflowed,
1863
+ "provenance_maintenance_work_cursor": String(
1864
+ capture.provenanceMaintenanceWorkCursor
1865
+ ),
1866
+ ]
1867
+ }
1868
+
1869
+ private func bridgeStatements(_ statements: [[String: Any]]) throws -> [Synchro.SQLStatement] {
1870
+ try statements.enumerated().map { index, item in
1871
+ guard let sql = item["sql"] as? String else {
1872
+ throw SynchroError.invalidResponse(message: "Missing SQL at index \(index)")
1873
+ }
1874
+ let params = try (item["params"] as? [Any]).map(bridgeParams)
1875
+ return Synchro.SQLStatement(sql: sql, params: params)
1876
+ }
1877
+ }
1878
+
1776
1879
  private func encodeBridgeJSON(_ value: Any) throws -> String {
1777
1880
  guard JSONSerialization.isValidJSONObject(value) else {
1778
1881
  throw SynchroError.invalidResponse(message: "bridge JSON value is invalid")
@@ -1784,6 +1887,26 @@ public class SynchroModuleImpl: NSObject {
1784
1887
  return json
1785
1888
  }
1786
1889
 
1890
+ private func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
1891
+ switch value {
1892
+ case .current(let mutation):
1893
+ return pendingMutationPayload(mutation).merging(["representation": "current"]) { current, _ in current }
1894
+ case .legacy(let mutation):
1895
+ return [
1896
+ "representation": "legacy",
1897
+ "mutationID": mutation.mutationID,
1898
+ "localOrder": mutation.localOrder,
1899
+ "tableName": mutation.tableName,
1900
+ "recordID": mutation.recordID,
1901
+ "operation": mutation.operation.rawValue,
1902
+ "baseVersion": mutation.baseVersion ?? NSNull(),
1903
+ "clientVersion": mutation.clientVersion,
1904
+ "status": mutation.status.rawValue,
1905
+ "sourceKind": mutation.sourceKind
1906
+ ]
1907
+ }
1908
+ }
1909
+
1787
1910
  private func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
1788
1911
  [
1789
1912
  "mutationID": mutation.mutationID,
@@ -1816,6 +1939,27 @@ public class SynchroModuleImpl: NSObject {
1816
1939
  ]
1817
1940
  }
1818
1941
 
1942
+ private func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
1943
+ switch value {
1944
+ case .current(let rejection):
1945
+ return rejectedMutationPayload(rejection).merging(["representation": "current"]) { current, _ in current }
1946
+ case .legacy(let rejection):
1947
+ return [
1948
+ "representation": "legacy",
1949
+ "mutationID": rejection.mutationID,
1950
+ "tableName": rejection.tableName,
1951
+ "recordID": rejection.recordID,
1952
+ "status": rejection.status.rawValue,
1953
+ "code": rejection.code.rawValue,
1954
+ "message": rejection.message ?? NSNull(),
1955
+ "serverRowJSON": rejection.serverRowJSON ?? NSNull(),
1956
+ "serverVersion": rejection.serverVersion ?? NSNull(),
1957
+ "createdAt": rejection.createdAt,
1958
+ "updatedAt": rejection.updatedAt
1959
+ ]
1960
+ }
1961
+ }
1962
+
1819
1963
  private func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
1820
1964
  [
1821
1965
  "mutationID": mutation.mutationID,