@trainstar/synchro-react-native 0.3.2 → 0.4.0-rc.2

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 (50) hide show
  1. package/README.md +112 -7
  2. package/SynchroReactNative.podspec +18 -1
  3. package/android/build.gradle +5 -1
  4. package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/SynchroModuleTransactionTest.kt +593 -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 +328 -112
  7. package/ios/SynchroModule.mm +27 -4
  8. package/ios/SynchroModule.swift +251 -89
  9. package/ios-test-host/main.m +48 -0
  10. package/ios-tests/SynchroModuleTransactionTests.swift +709 -0
  11. package/ios-tests/TransactionSessionTests.swift +66 -0
  12. package/lib/module/NativeSynchro.js.map +1 -1
  13. package/lib/module/SynchroClient.js +141 -2
  14. package/lib/module/SynchroClient.js.map +1 -1
  15. package/lib/module/errors.js +15 -1
  16. package/lib/module/errors.js.map +1 -1
  17. package/lib/module/index.js +1 -1
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/inspection.js +99 -8
  20. package/lib/module/inspection.js.map +1 -1
  21. package/lib/module/types.js +18 -0
  22. package/lib/module/types.js.map +1 -1
  23. package/lib/typescript/example/e2e/schema-check.test.d.ts +2 -0
  24. package/lib/typescript/example/e2e/schema-check.test.d.ts.map +1 -0
  25. package/lib/typescript/example/src/conformance/runner.d.ts +17 -2
  26. package/lib/typescript/example/src/conformance/runner.d.ts.map +1 -1
  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 +23 -5
  36. package/lib/typescript/src/inspection.d.ts.map +1 -1
  37. package/lib/typescript/src/types.d.ts +62 -1
  38. package/lib/typescript/src/types.d.ts.map +1 -1
  39. package/package.json +21 -6
  40. package/src/NativeSynchro.ts +6 -1
  41. package/src/SynchroClient.ts +160 -1
  42. package/src/errors.ts +30 -0
  43. package/src/index.ts +7 -0
  44. package/src/inspection.ts +138 -9
  45. package/src/types.ts +70 -1
  46. package/tooling-patches/apply.cjs +163 -0
  47. package/tooling-patches/cli-clean-20.2.0.patch +21 -0
  48. package/tooling-patches/cli-config-android-20.2.0.patch +13 -0
  49. package/tooling-patches/react-native-builder-bob-0.40.18.patch +74 -0
  50. package/tooling-patches/stream-json-1.9.1.patch +40 -0
@@ -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,66 @@ 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
+ "migration_journal": try snapshot.capture.migrationJournal.map {
1639
+ try JSONSerialization.jsonObject(with: JSONEncoder().encode($0))
1640
+ } ?? NSNull(),
1641
+ "migration_journal_truncated": snapshot.capture.migrationJournalTruncated,
1642
+ "physical_schema": try JSONSerialization.jsonObject(with: JSONEncoder().encode(snapshot.capture.physicalSchema)),
1643
+ "physical_schema_truncated": snapshot.capture.physicalSchemaTruncated,
1644
+ "accepted_mutation_outcomes": snapshot.capture.acceptedMutationOutcomes,
1645
+ "accepted_mutation_outcomes_truncated": snapshot.capture.acceptedMutationOutcomesTruncated,
1646
+ "retained_mutations": snapshot.retainedMutations.map { $0.map(retainedMutationPayload) } ?? NSNull(),
1647
+ "rejected_mutations": snapshot.rejectedMutations.map { $0.map(retainedRejectionPayload) } ?? NSNull(),
1648
+ ]
1649
+ resolve([
1650
+ "inspection": try encodeBridgeJSON(inspection),
1651
+ "applicationRows": rowsToBridgeRows(rows),
1652
+ ])
1616
1653
  } catch {
1617
1654
  rejectWithError(reject, error)
1618
1655
  }
1619
1656
  }
1620
1657
 
1658
+ /// Reads one record's row metadata and every rebuild receipt. The inspection snapshot bounds
1659
+ /// row metadata, so a durable proof reads its identity directly.
1621
1660
  @objc
1622
1661
  public func inspectDurableState(
1623
1662
  _ tableName: String,
@@ -1693,13 +1732,18 @@ public class SynchroModuleImpl: NSObject {
1693
1732
  resolve: @escaping RCTPromiseResolveBlock,
1694
1733
  reject: @escaping RCTPromiseRejectBlock
1695
1734
  ) {
1696
- guard let collector = transportObservations,
1697
- let operation = TransportOperationClass(rawValue: operationClass) else {
1735
+ guard let collector = transportObservations else {
1698
1736
  reject("INVALID_CONFIG", "Transport pause operation is invalid", nil)
1699
1737
  return
1700
1738
  }
1701
1739
  do {
1702
- try collector.armPause(for: operation)
1740
+ if let checkpoint = MigrationCheckpoint(rawValue: operationClass) {
1741
+ try collector.armPause(for: checkpoint)
1742
+ } else if let operation = TransportOperationClass(rawValue: operationClass) {
1743
+ try collector.armPause(for: operation)
1744
+ } else {
1745
+ throw SynchroError.invalidResponse(message: "pause target is invalid")
1746
+ }
1703
1747
  resolve(nil)
1704
1748
  } catch {
1705
1749
  rejectWithError(reject, error)
@@ -1714,14 +1758,19 @@ public class SynchroModuleImpl: NSObject {
1714
1758
  reject: @escaping RCTPromiseRejectBlock
1715
1759
  ) {
1716
1760
  guard let collector = transportObservations,
1717
- let operation = TransportOperationClass(rawValue: operationClass),
1718
1761
  timeoutMs.isFinite, timeoutMs >= 1, timeoutMs <= 60_000 else {
1719
1762
  reject("INVALID_CONFIG", "Transport pause wait is invalid", nil)
1720
1763
  return
1721
1764
  }
1722
1765
  Task {
1723
1766
  do {
1724
- try await collector.awaitPause(for: operation, timeout: timeoutMs / 1_000)
1767
+ if let checkpoint = MigrationCheckpoint(rawValue: operationClass) {
1768
+ try await collector.awaitPause(for: checkpoint, timeout: timeoutMs / 1_000)
1769
+ } else if let operation = TransportOperationClass(rawValue: operationClass) {
1770
+ try await collector.awaitPause(for: operation, timeout: timeoutMs / 1_000)
1771
+ } else {
1772
+ throw SynchroError.invalidResponse(message: "pause target is invalid")
1773
+ }
1725
1774
  DispatchQueue.main.async { resolve(nil) }
1726
1775
  } catch {
1727
1776
  DispatchQueue.main.async { self.rejectWithError(reject, error) }
@@ -1773,6 +1822,78 @@ public class SynchroModuleImpl: NSObject {
1773
1822
 
1774
1823
  // MARK: - Helpers
1775
1824
 
1825
+ private func clientStatePayload(_ capture: ClientStateCaptureInspection) -> [String: Any] {
1826
+ let schema: Any = capture.schema.map { value in
1827
+ ["version": value.version, "hash": value.hash]
1828
+ } ?? NSNull()
1829
+ let scopeStates = capture.scopeStates.map { value in
1830
+ [
1831
+ "scope_id": value.scopeID,
1832
+ "cursor": value.cursor ?? NSNull(),
1833
+ "checksum": value.checksum ?? NSNull(),
1834
+ "local_checksum": value.localChecksum,
1835
+ "generation": value.generation,
1836
+ ] as [String: Any]
1837
+ }
1838
+ let scopeRows = capture.scopeRows.map { value in
1839
+ [
1840
+ "scope_id": value.scopeID,
1841
+ "table_name": value.tableName,
1842
+ "record_id": value.recordID,
1843
+ "checksum": value.checksum,
1844
+ "generation": value.generation,
1845
+ ] as [String: Any]
1846
+ }
1847
+ let attempts = capture.rebuildAttempts.map { value in
1848
+ [
1849
+ "scope_id": value.scopeID,
1850
+ "rebuild_id": value.rebuildID,
1851
+ "client_generation": value.clientGeneration,
1852
+ "schema_version": value.schemaVersion,
1853
+ "schema_hash": value.schemaHash,
1854
+ "generation": value.generation,
1855
+ "cursor": value.cursor ?? NSNull(),
1856
+ "page_limit": value.pageLimit,
1857
+ ] as [String: Any]
1858
+ }
1859
+ return [
1860
+ "schema": schema,
1861
+ "scope_states": scopeStates,
1862
+ "scope_rows": scopeRows,
1863
+ "rebuild_attempts": attempts,
1864
+ "application_row_count": capture.applicationRowCount,
1865
+ "mutation_ledger_count": capture.mutationLedgerCount,
1866
+ "mutation_outcome_count": capture.mutationOutcomeCount,
1867
+ "sealed_batch_count": capture.sealedBatchCount,
1868
+ "rejected_mutation_count": capture.rejectedMutationCount,
1869
+ "scope_state_count": capture.scopeStateCount,
1870
+ "scope_row_count": capture.scopeRowCount,
1871
+ "provenance_count": capture.provenanceCount,
1872
+ "row_metadata_count": capture.rowMetadataCount,
1873
+ "rebuild_attempt_count": capture.rebuildAttemptCount,
1874
+ "rebuild_receipt_count": capture.rebuildReceiptCount,
1875
+ "scope_states_truncated": capture.scopeStatesTruncated,
1876
+ "scope_rows_truncated": capture.scopeRowsTruncated,
1877
+ "rebuild_attempts_truncated": capture.rebuildAttemptsTruncated,
1878
+ "rebuild_receipts_truncated": capture.rebuildReceiptsTruncated,
1879
+ "row_metadata_truncated": capture.rowMetadataTruncated,
1880
+ "capture_overflowed": capture.overflowed,
1881
+ "provenance_maintenance_work_cursor": String(
1882
+ capture.provenanceMaintenanceWorkCursor
1883
+ ),
1884
+ ]
1885
+ }
1886
+
1887
+ private func bridgeStatements(_ statements: [[String: Any]]) throws -> [Synchro.SQLStatement] {
1888
+ try statements.enumerated().map { index, item in
1889
+ guard let sql = item["sql"] as? String else {
1890
+ throw SynchroError.invalidResponse(message: "Missing SQL at index \(index)")
1891
+ }
1892
+ let params = try (item["params"] as? [Any]).map(bridgeParams)
1893
+ return Synchro.SQLStatement(sql: sql, params: params)
1894
+ }
1895
+ }
1896
+
1776
1897
  private func encodeBridgeJSON(_ value: Any) throws -> String {
1777
1898
  guard JSONSerialization.isValidJSONObject(value) else {
1778
1899
  throw SynchroError.invalidResponse(message: "bridge JSON value is invalid")
@@ -1784,6 +1905,26 @@ public class SynchroModuleImpl: NSObject {
1784
1905
  return json
1785
1906
  }
1786
1907
 
1908
+ private func retainedMutationPayload(_ value: RetainedMutationInspection) -> [String: Any] {
1909
+ switch value {
1910
+ case .current(let mutation):
1911
+ return pendingMutationPayload(mutation).merging(["representation": "current"]) { current, _ in current }
1912
+ case .legacy(let mutation):
1913
+ return [
1914
+ "representation": "legacy",
1915
+ "mutationID": mutation.mutationID,
1916
+ "localOrder": mutation.localOrder,
1917
+ "tableName": mutation.tableName,
1918
+ "recordID": mutation.recordID,
1919
+ "operation": mutation.operation.rawValue,
1920
+ "baseVersion": mutation.baseVersion ?? NSNull(),
1921
+ "clientVersion": mutation.clientVersion,
1922
+ "status": mutation.status.rawValue,
1923
+ "sourceKind": mutation.sourceKind
1924
+ ]
1925
+ }
1926
+ }
1927
+
1787
1928
  private func pendingMutationPayload(_ mutation: PendingMutationInspection) -> [String: Any] {
1788
1929
  [
1789
1930
  "mutationID": mutation.mutationID,
@@ -1816,6 +1957,27 @@ public class SynchroModuleImpl: NSObject {
1816
1957
  ]
1817
1958
  }
1818
1959
 
1960
+ private func retainedRejectionPayload(_ value: RetainedRejectionInspection) -> [String: Any] {
1961
+ switch value {
1962
+ case .current(let rejection):
1963
+ return rejectedMutationPayload(rejection).merging(["representation": "current"]) { current, _ in current }
1964
+ case .legacy(let rejection):
1965
+ return [
1966
+ "representation": "legacy",
1967
+ "mutationID": rejection.mutationID,
1968
+ "tableName": rejection.tableName,
1969
+ "recordID": rejection.recordID,
1970
+ "status": rejection.status.rawValue,
1971
+ "code": rejection.code.rawValue,
1972
+ "message": rejection.message ?? NSNull(),
1973
+ "serverRowJSON": rejection.serverRowJSON ?? NSNull(),
1974
+ "serverVersion": rejection.serverVersion ?? NSNull(),
1975
+ "createdAt": rejection.createdAt,
1976
+ "updatedAt": rejection.updatedAt
1977
+ ]
1978
+ }
1979
+ }
1980
+
1819
1981
  private func rejectedMutationPayload(_ mutation: RejectedMutationInspection) -> [String: Any] {
1820
1982
  [
1821
1983
  "mutationID": mutation.mutationID,
@@ -0,0 +1,48 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ @interface SynchroTestHostSceneDelegate : UIResponder <UIWindowSceneDelegate>
4
+ @property (nonatomic, strong) UIWindow *window;
5
+ @end
6
+
7
+ @implementation SynchroTestHostSceneDelegate
8
+
9
+ - (void)scene:(UIScene *)scene
10
+ willConnectToSession:(UISceneSession *)session
11
+ options:(UISceneConnectionOptions *)connectionOptions
12
+ {
13
+ if (![scene isKindOfClass:[UIWindowScene class]]) {
14
+ return;
15
+ }
16
+
17
+ self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
18
+ self.window.rootViewController = [UIViewController new];
19
+ [self.window makeKeyAndVisible];
20
+ }
21
+
22
+ @end
23
+
24
+ @interface SynchroTestHostAppDelegate : UIResponder <UIApplicationDelegate>
25
+ @end
26
+
27
+ @implementation SynchroTestHostAppDelegate
28
+
29
+ - (UISceneConfiguration *)application:(UIApplication *)application
30
+ configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
31
+ options:(UISceneConnectionOptions *)options
32
+ {
33
+ UISceneConfiguration *configuration = [[UISceneConfiguration alloc]
34
+ initWithName:@"Default Configuration"
35
+ sessionRole:connectingSceneSession.role];
36
+ configuration.sceneClass = [UIWindowScene class];
37
+ configuration.delegateClass = [SynchroTestHostSceneDelegate class];
38
+ return configuration;
39
+ }
40
+
41
+ @end
42
+
43
+ int main(int argc, char *argv[])
44
+ {
45
+ @autoreleasepool {
46
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([SynchroTestHostAppDelegate class]));
47
+ }
48
+ }