@trainstar/synchro-react-native 0.3.2 → 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.
- package/README.md +58 -4
- package/SynchroReactNative.podspec +6 -0
- package/android/build.gradle +5 -1
- package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/SynchroModuleTransactionTest.kt +591 -0
- package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/TransactionSessionTest.kt +314 -0
- package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroModule.kt +306 -110
- package/ios/SynchroModule.mm +27 -4
- package/ios/SynchroModule.swift +228 -84
- package/ios-tests/SynchroModuleTransactionTests.swift +707 -0
- package/ios-tests/TransactionSessionTests.swift +66 -0
- package/lib/module/NativeSynchro.js.map +1 -1
- package/lib/module/SynchroClient.js +115 -2
- package/lib/module/SynchroClient.js.map +1 -1
- package/lib/module/errors.js +15 -1
- package/lib/module/errors.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/inspection.js +31 -3
- package/lib/module/inspection.js.map +1 -1
- package/lib/module/types.js +18 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/example/src/conformance/runner.d.ts +1 -0
- package/lib/typescript/example/src/conformance/runner.d.ts.map +1 -1
- package/lib/typescript/src/NativeSynchro.d.ts +7 -1
- package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
- package/lib/typescript/src/SynchroClient.d.ts +29 -1
- package/lib/typescript/src/SynchroClient.d.ts.map +1 -1
- package/lib/typescript/src/errors.d.ts +7 -1
- package/lib/typescript/src/errors.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/inspection.d.ts +13 -2
- package/lib/typescript/src/inspection.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +44 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/NativeSynchro.ts +6 -1
- package/src/SynchroClient.ts +128 -1
- package/src/errors.ts +30 -0
- package/src/index.ts +7 -0
- package/src/inspection.ts +44 -2
- package/src/types.ts +49 -1
|
@@ -8,6 +8,7 @@ import android.util.Base64
|
|
|
8
8
|
import android.os.Process
|
|
9
9
|
import com.facebook.react.bridge.*
|
|
10
10
|
import com.trainstar.synchro.*
|
|
11
|
+
import com.trainstar.synchro.inspection.ClientStateCaptureInspection
|
|
11
12
|
import com.trainstar.synchro.inspection.SynchroInspection
|
|
12
13
|
import com.trainstar.synchro.inspection.TransportObservationCollector
|
|
13
14
|
import com.trainstar.synchro.inspection.TransportObservationSnapshot
|
|
@@ -50,11 +51,12 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
50
51
|
val INT64_PATTERN = Regex("^(?:0|-?[1-9][0-9]*)$")
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
internal var client: SynchroClient? = null
|
|
55
|
+
private set
|
|
54
56
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
55
57
|
private val lifecycleMutex = Mutex()
|
|
56
58
|
private val transactionLock = Any()
|
|
57
|
-
|
|
59
|
+
internal val sessions = ConcurrentHashMap<String, TransactionSession>()
|
|
58
60
|
private var acceptingTransactions = false
|
|
59
61
|
private val observers = ConcurrentHashMap<String, Cancellable>()
|
|
60
62
|
private val pendingAuthContinuations = ConcurrentHashMap<String, CancellableContinuation<String>>()
|
|
@@ -136,6 +138,9 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
136
138
|
)
|
|
137
139
|
is SynchroError.AlreadyStarted -> "ALREADY_STARTED" to emptyMap()
|
|
138
140
|
is SynchroError.NotStarted -> "NOT_STARTED" to emptyMap()
|
|
141
|
+
is SynchroError.AtomicGroupInvalid -> "atomic_group_invalid" to mapOf(
|
|
142
|
+
"reason" to atomicGroupInvalidReasonWireValue(error.reason)
|
|
143
|
+
)
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -404,13 +409,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
404
409
|
return
|
|
405
410
|
}
|
|
406
411
|
try {
|
|
407
|
-
val
|
|
408
|
-
val item = statements.getMap(i) ?: throw IllegalArgumentException("Invalid SQL statement at index $i")
|
|
409
|
-
val sql = item.getString("sql") ?: throw IllegalArgumentException("Missing SQL at index $i")
|
|
410
|
-
val params = if (item.hasKey("params")) item.getArray("params") else null
|
|
411
|
-
SQLStatement(sql, params?.let { parseParams(it) } ?: emptyArray())
|
|
412
|
-
}
|
|
413
|
-
val total = c.executeBatch(nativeStatements)
|
|
412
|
+
val total = c.executeBatch(parseStatements(statements))
|
|
414
413
|
val map = Arguments.createMap().apply {
|
|
415
414
|
putInt("totalRowsAffected", total)
|
|
416
415
|
}
|
|
@@ -422,11 +421,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
422
421
|
|
|
423
422
|
// MARK: - Transactions
|
|
424
423
|
|
|
425
|
-
|
|
424
|
+
internal class TransactionSession(
|
|
425
|
+
val isWrite: Boolean,
|
|
426
|
+
private var beginSettlement: ((Result<String>) -> Unit)?,
|
|
427
|
+
) {
|
|
426
428
|
val operations = Channel<TransactionOp>(Channel.RENDEZVOUS)
|
|
427
429
|
lateinit var job: Job
|
|
428
430
|
private var abortCause: Throwable? = null
|
|
429
431
|
private var terminal = false
|
|
432
|
+
private var terminalCompletion: CompletableDeferred<Unit>? = null
|
|
433
|
+
|
|
434
|
+
// A cancelled receive can drop an operation that its sender already handed over.
|
|
435
|
+
// The session owns each submitted operation until its completion settles.
|
|
436
|
+
private val outstanding = mutableSetOf<TransactionOp>()
|
|
430
437
|
|
|
431
438
|
@Synchronized
|
|
432
439
|
fun abort(cause: Throwable) {
|
|
@@ -436,16 +443,106 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
436
443
|
operations.close(cause)
|
|
437
444
|
}
|
|
438
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Ends the session after its transaction returns. Each operation that the loop did not
|
|
448
|
+
* complete, received or not, settles with the abort cause. A rollback settles successfully.
|
|
449
|
+
*/
|
|
450
|
+
@Synchronized
|
|
451
|
+
fun end() {
|
|
452
|
+
if (!terminal) {
|
|
453
|
+
terminal = true
|
|
454
|
+
operations.close()
|
|
455
|
+
}
|
|
456
|
+
outstanding.toList().forEach(::settleUnrun)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Submits one operation. A session that already ended settles it at once. */
|
|
460
|
+
suspend fun <T> perform(op: TransactionOp, deferred: CompletableDeferred<T>): T {
|
|
461
|
+
if (submit(op)) {
|
|
462
|
+
try {
|
|
463
|
+
operations.send(op)
|
|
464
|
+
} catch (_: Exception) {
|
|
465
|
+
// The channel closed or dropped the operation. The session settles it when it ends.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return deferred.await()
|
|
469
|
+
}
|
|
470
|
+
|
|
439
471
|
@Synchronized
|
|
440
|
-
fun
|
|
472
|
+
fun submit(op: TransactionOp): Boolean {
|
|
473
|
+
if (terminal) {
|
|
474
|
+
settleUnrun(op)
|
|
475
|
+
return false
|
|
476
|
+
}
|
|
477
|
+
outstanding += op
|
|
478
|
+
return true
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private fun settleUnrun(op: TransactionOp) {
|
|
482
|
+
outstanding -= op
|
|
483
|
+
when (op) {
|
|
484
|
+
is TransactionOp.Rollback -> op.deferred.complete(Unit)
|
|
485
|
+
else -> op.deferred.completeExceptionally(abortCause ?: TransactionAbortedException())
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private fun release(deferred: CompletableDeferred<*>) {
|
|
490
|
+
outstanding.removeAll { it.deferred === deferred }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Settles begin when the transaction callback starts.
|
|
495
|
+
* A close that aborted the session earlier rejects begin instead.
|
|
496
|
+
*/
|
|
497
|
+
@Synchronized
|
|
498
|
+
fun acceptBegin(txID: String) {
|
|
499
|
+
val settle = beginSettlement
|
|
500
|
+
beginSettlement = null
|
|
501
|
+
val cause = abortCause
|
|
502
|
+
if (cause != null) {
|
|
503
|
+
settle?.invoke(Result.failure(cause))
|
|
504
|
+
throw cause
|
|
505
|
+
}
|
|
506
|
+
settle?.invoke(Result.success(txID))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Rejects begin when the transaction fails before its callback starts. */
|
|
510
|
+
@Synchronized
|
|
511
|
+
fun rejectBegin(error: Throwable) {
|
|
512
|
+
val settle = beginSettlement ?: return
|
|
513
|
+
beginSettlement = null
|
|
514
|
+
settle(Result.failure(error))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Takes ownership of a received commit or rollback completion before the terminal decision.
|
|
519
|
+
* The job settles the owned completion after the database transaction ends.
|
|
520
|
+
*/
|
|
521
|
+
@Synchronized
|
|
522
|
+
fun acceptTerminal(completion: CompletableDeferred<Unit>): Boolean {
|
|
523
|
+
release(completion)
|
|
524
|
+
terminalCompletion = completion
|
|
441
525
|
if (terminal) return false
|
|
442
526
|
terminal = true
|
|
443
527
|
operations.close()
|
|
444
528
|
return true
|
|
445
529
|
}
|
|
446
530
|
|
|
531
|
+
/** Settles the owned terminal completion after the database transaction ends. */
|
|
532
|
+
@Synchronized
|
|
533
|
+
fun finishTerminal(error: Throwable?) {
|
|
534
|
+
val completion = terminalCompletion ?: return
|
|
535
|
+
terminalCompletion = null
|
|
536
|
+
if (error == null) {
|
|
537
|
+
completion.complete(Unit)
|
|
538
|
+
} else {
|
|
539
|
+
completion.completeExceptionally(error)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
447
543
|
@Synchronized
|
|
448
544
|
fun <T> complete(deferred: CompletableDeferred<T>, value: T) {
|
|
545
|
+
release(deferred)
|
|
449
546
|
val cause = abortCause
|
|
450
547
|
if (cause == null) {
|
|
451
548
|
deferred.complete(value)
|
|
@@ -456,21 +553,22 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
456
553
|
|
|
457
554
|
@Synchronized
|
|
458
555
|
fun <T> completeExceptionally(deferred: CompletableDeferred<T>, error: Throwable) {
|
|
556
|
+
release(deferred)
|
|
459
557
|
deferred.completeExceptionally(abortCause ?: error)
|
|
460
558
|
}
|
|
461
559
|
}
|
|
462
560
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
class
|
|
468
|
-
class
|
|
561
|
+
internal sealed class TransactionOp {
|
|
562
|
+
abstract val deferred: CompletableDeferred<*>
|
|
563
|
+
|
|
564
|
+
class Query(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableArray>) : TransactionOp()
|
|
565
|
+
class QueryOne(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableMap?>) : TransactionOp()
|
|
566
|
+
class Execute(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableMap>) : TransactionOp()
|
|
567
|
+
class Commit(override val deferred: CompletableDeferred<Unit>) : TransactionOp()
|
|
568
|
+
class Rollback(override val deferred: CompletableDeferred<Unit>) : TransactionOp()
|
|
469
569
|
}
|
|
470
570
|
|
|
471
|
-
|
|
472
|
-
val completion: CompletableDeferred<Unit>,
|
|
473
|
-
) : Exception("rollback")
|
|
571
|
+
internal class TransactionRollbackException : Exception("rollback")
|
|
474
572
|
|
|
475
573
|
private class TransactionAbortedException : Exception("Client closed during transaction")
|
|
476
574
|
private class TransactionExpiredException : Exception("Transaction timed out due to inactivity")
|
|
@@ -480,12 +578,17 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
480
578
|
beginTransaction(isWrite = true, promise = promise)
|
|
481
579
|
}
|
|
482
580
|
|
|
581
|
+
@ReactMethod
|
|
582
|
+
override fun beginAtomicWriteTransaction(promise: Promise) {
|
|
583
|
+
beginTransaction(isWrite = true, isAtomic = true, promise = promise)
|
|
584
|
+
}
|
|
585
|
+
|
|
483
586
|
@ReactMethod
|
|
484
587
|
override fun beginReadTransaction(promise: Promise) {
|
|
485
588
|
beginTransaction(isWrite = false, promise = promise)
|
|
486
589
|
}
|
|
487
590
|
|
|
488
|
-
private fun beginTransaction(isWrite: Boolean, promise: Promise) {
|
|
591
|
+
private fun beginTransaction(isWrite: Boolean, promise: Promise, isAtomic: Boolean = false) {
|
|
489
592
|
val c = synchronized(transactionLock) {
|
|
490
593
|
client.takeIf { acceptingTransactions }
|
|
491
594
|
} ?: run {
|
|
@@ -494,28 +597,31 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
494
597
|
}
|
|
495
598
|
|
|
496
599
|
val txID = UUID.randomUUID().toString()
|
|
497
|
-
val session = TransactionSession(isWrite)
|
|
600
|
+
val session = TransactionSession(isWrite) { result ->
|
|
601
|
+
result.fold(
|
|
602
|
+
onSuccess = { promise.resolve(it) },
|
|
603
|
+
onFailure = { rejectWithError(promise, it) },
|
|
604
|
+
)
|
|
605
|
+
}
|
|
498
606
|
|
|
499
607
|
val job = scope.launch(start = CoroutineStart.LAZY) {
|
|
500
|
-
var finalDeferred: CompletableDeferred<Unit>? = null
|
|
501
608
|
try {
|
|
502
609
|
if (isWrite) {
|
|
503
|
-
|
|
504
|
-
|
|
610
|
+
val body: (ApplicationTransaction) -> Unit = { transaction ->
|
|
611
|
+
runTransactionLoop(
|
|
505
612
|
txID = txID,
|
|
506
613
|
session = session,
|
|
507
|
-
promise = promise,
|
|
508
614
|
query = transaction::query,
|
|
509
615
|
queryOne = transaction::queryOne,
|
|
510
616
|
execute = transaction::execute,
|
|
511
617
|
)
|
|
512
618
|
}
|
|
619
|
+
if (isAtomic) c.atomicWriteTransaction(body) else c.writeTransaction(body)
|
|
513
620
|
} else {
|
|
514
621
|
c.readTransaction { transaction ->
|
|
515
|
-
|
|
622
|
+
runTransactionLoop(
|
|
516
623
|
txID = txID,
|
|
517
624
|
session = session,
|
|
518
|
-
promise = promise,
|
|
519
625
|
query = transaction::query,
|
|
520
626
|
queryOne = transaction::queryOne,
|
|
521
627
|
execute = { _, _ ->
|
|
@@ -524,17 +630,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
524
630
|
)
|
|
525
631
|
}
|
|
526
632
|
}
|
|
527
|
-
|
|
633
|
+
session.finishTerminal(null)
|
|
528
634
|
} catch (e: TimeoutCancellationException) {
|
|
529
635
|
val timeout = TransactionExpiredException()
|
|
530
636
|
session.abort(timeout)
|
|
531
|
-
|
|
637
|
+
session.finishTerminal(timeout)
|
|
532
638
|
} catch (e: TransactionRollbackException) {
|
|
533
|
-
|
|
639
|
+
session.finishTerminal(null)
|
|
534
640
|
} catch (e: Exception) {
|
|
535
|
-
|
|
641
|
+
// Acquisition can fail before the callback settles begin.
|
|
642
|
+
session.rejectBegin(e)
|
|
643
|
+
session.finishTerminal(e)
|
|
536
644
|
} finally {
|
|
537
|
-
session.
|
|
645
|
+
session.end()
|
|
538
646
|
sessions.remove(txID, session)
|
|
539
647
|
}
|
|
540
648
|
}
|
|
@@ -555,16 +663,14 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
555
663
|
job.start()
|
|
556
664
|
}
|
|
557
665
|
|
|
558
|
-
|
|
666
|
+
internal fun runTransactionLoop(
|
|
559
667
|
txID: String,
|
|
560
668
|
session: TransactionSession,
|
|
561
|
-
promise: Promise,
|
|
562
669
|
query: (String, Array<out Any?>?) -> List<Row>,
|
|
563
670
|
queryOne: (String, Array<out Any?>?) -> Row?,
|
|
564
671
|
execute: (String, Array<out Any?>?) -> ExecResult,
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
var finalDeferred: CompletableDeferred<Unit>? = null
|
|
672
|
+
) {
|
|
673
|
+
session.acceptBegin(txID)
|
|
568
674
|
runBlocking {
|
|
569
675
|
transactionLoop@ while (true) {
|
|
570
676
|
val result = withTimeout(5000) { session.operations.receiveCatching() }
|
|
@@ -604,19 +710,16 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
604
710
|
}
|
|
605
711
|
}
|
|
606
712
|
is TransactionOp.Commit -> {
|
|
607
|
-
|
|
608
|
-
if (!session.completeNormally()) throw TransactionAbortedException()
|
|
713
|
+
if (!session.acceptTerminal(op.deferred)) throw TransactionAbortedException()
|
|
609
714
|
break@transactionLoop
|
|
610
715
|
}
|
|
611
716
|
is TransactionOp.Rollback -> {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
throw TransactionRollbackException(op.deferred)
|
|
717
|
+
if (!session.acceptTerminal(op.deferred)) throw TransactionAbortedException()
|
|
718
|
+
throw TransactionRollbackException()
|
|
615
719
|
}
|
|
616
720
|
}
|
|
617
721
|
}
|
|
618
722
|
}
|
|
619
|
-
return finalDeferred
|
|
620
723
|
}
|
|
621
724
|
|
|
622
725
|
@ReactMethod
|
|
@@ -628,8 +731,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
628
731
|
scope.launch {
|
|
629
732
|
try {
|
|
630
733
|
val deferred = CompletableDeferred<WritableArray>()
|
|
631
|
-
session.
|
|
632
|
-
promise.resolve(deferred.await())
|
|
734
|
+
promise.resolve(session.perform(TransactionOp.Query(sql, parseParams(params), deferred), deferred))
|
|
633
735
|
} catch (e: Exception) {
|
|
634
736
|
rejectWithError(promise, e)
|
|
635
737
|
}
|
|
@@ -645,8 +747,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
645
747
|
scope.launch {
|
|
646
748
|
try {
|
|
647
749
|
val deferred = CompletableDeferred<WritableMap?>()
|
|
648
|
-
session.
|
|
649
|
-
promise.resolve(deferred.await())
|
|
750
|
+
promise.resolve(session.perform(TransactionOp.QueryOne(sql, parseParams(params), deferred), deferred))
|
|
650
751
|
} catch (e: Exception) {
|
|
651
752
|
rejectWithError(promise, e)
|
|
652
753
|
}
|
|
@@ -666,8 +767,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
666
767
|
scope.launch {
|
|
667
768
|
try {
|
|
668
769
|
val deferred = CompletableDeferred<WritableMap>()
|
|
669
|
-
session.
|
|
670
|
-
promise.resolve(deferred.await())
|
|
770
|
+
promise.resolve(session.perform(TransactionOp.Execute(sql, parseParams(params), deferred), deferred))
|
|
671
771
|
} catch (e: Exception) {
|
|
672
772
|
rejectWithError(promise, e)
|
|
673
773
|
}
|
|
@@ -683,8 +783,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
683
783
|
scope.launch {
|
|
684
784
|
try {
|
|
685
785
|
val deferred = CompletableDeferred<Unit>()
|
|
686
|
-
session.
|
|
687
|
-
deferred.await()
|
|
786
|
+
session.perform(TransactionOp.Commit(deferred), deferred)
|
|
688
787
|
promise.resolve(null)
|
|
689
788
|
} catch (e: Exception) {
|
|
690
789
|
rejectWithError(promise, e)
|
|
@@ -700,12 +799,12 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
700
799
|
}
|
|
701
800
|
scope.launch {
|
|
702
801
|
try {
|
|
802
|
+
// A session that ends before it runs this rollback settles it successfully.
|
|
703
803
|
val deferred = CompletableDeferred<Unit>()
|
|
704
|
-
session.
|
|
705
|
-
deferred.await()
|
|
804
|
+
session.perform(TransactionOp.Rollback(deferred), deferred)
|
|
706
805
|
promise.resolve(null)
|
|
707
806
|
} catch (e: Exception) {
|
|
708
|
-
promise
|
|
807
|
+
rejectWithError(promise, e)
|
|
709
808
|
}
|
|
710
809
|
}
|
|
711
810
|
}
|
|
@@ -972,6 +1071,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
972
1071
|
}
|
|
973
1072
|
}
|
|
974
1073
|
|
|
1074
|
+
@ReactMethod
|
|
1075
|
+
override fun inspectRetainedMutationRecords(promise: Promise) {
|
|
1076
|
+
val c = client ?: run {
|
|
1077
|
+
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
promise.resolve(JSONArray(c.inspectRetainedMutationRecords().map(::retainedMutationJson)).toString())
|
|
1082
|
+
} catch (e: Exception) {
|
|
1083
|
+
rejectWithError(promise, e)
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
975
1087
|
@ReactMethod
|
|
976
1088
|
override fun inspectRejectedMutations(promise: Promise) {
|
|
977
1089
|
val c = client ?: run {
|
|
@@ -986,73 +1098,51 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
986
1098
|
}
|
|
987
1099
|
|
|
988
1100
|
@ReactMethod
|
|
989
|
-
override fun
|
|
1101
|
+
override fun inspectRejectedMutationRecords(promise: Promise) {
|
|
990
1102
|
val c = client ?: run {
|
|
991
1103
|
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
992
1104
|
return
|
|
993
1105
|
}
|
|
994
1106
|
try {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
val
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
put("checksum", value.checksum)
|
|
1015
|
-
put("generation", value.generation)
|
|
1016
|
-
}
|
|
1017
|
-
})
|
|
1018
|
-
val attempts = JSONArray(capture.rebuildAttempts.map { value ->
|
|
1019
|
-
JSONObject().apply {
|
|
1020
|
-
put("scope_id", value.scopeID)
|
|
1021
|
-
put("rebuild_id", value.rebuildID)
|
|
1022
|
-
put("client_generation", value.clientGeneration)
|
|
1023
|
-
put("schema_version", value.schemaVersion)
|
|
1024
|
-
put("schema_hash", value.schemaHash)
|
|
1025
|
-
put("generation", value.generation)
|
|
1026
|
-
put("cursor", value.cursor ?: JSONObject.NULL)
|
|
1027
|
-
put("page_limit", value.pageLimit)
|
|
1107
|
+
promise.resolve(JSONArray(c.inspectRejectedMutationRecords().map(::retainedRejectionJson)).toString())
|
|
1108
|
+
} catch (e: Exception) {
|
|
1109
|
+
rejectWithError(promise, e)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** Reads client state, retained details, and the requested application rows from one read-only snapshot. */
|
|
1114
|
+
@ReactMethod
|
|
1115
|
+
override fun inspectClientStateSnapshot(rowStatements: ReadableArray, promise: Promise) {
|
|
1116
|
+
val c = client ?: run {
|
|
1117
|
+
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
1118
|
+
return
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
val statements = parseStatements(rowStatements)
|
|
1122
|
+
val rows = mutableListOf<Row>()
|
|
1123
|
+
val snapshot = SynchroInspection(c).captureSnapshot(maximumRecords = 512) { _, transaction ->
|
|
1124
|
+
for (statement in statements) {
|
|
1125
|
+
rows += transaction.query(statement.sql, statement.params)
|
|
1028
1126
|
}
|
|
1127
|
+
}
|
|
1128
|
+
val inspection = JSONObject().apply {
|
|
1129
|
+
put("client_state", clientStateJson(snapshot.capture))
|
|
1130
|
+
put("retained_mutations", snapshot.retainedMutations?.let { JSONArray(it.map(::retainedMutationJson)) } ?: JSONObject.NULL)
|
|
1131
|
+
put("rejected_mutations", snapshot.rejectedMutations?.let { JSONArray(it.map(::retainedRejectionJson)) } ?: JSONObject.NULL)
|
|
1132
|
+
}
|
|
1133
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1134
|
+
putString("inspection", inspection.toString())
|
|
1135
|
+
putArray("applicationRows", rowsToWritableArray(rows))
|
|
1029
1136
|
})
|
|
1030
|
-
promise.resolve(JSONObject().apply {
|
|
1031
|
-
put("schema", schema)
|
|
1032
|
-
put("scope_states", scopeStates)
|
|
1033
|
-
put("scope_rows", scopeRows)
|
|
1034
|
-
put("rebuild_attempts", attempts)
|
|
1035
|
-
put("application_row_count", capture.applicationRowCount)
|
|
1036
|
-
put("mutation_ledger_count", capture.mutationLedgerCount)
|
|
1037
|
-
put("mutation_outcome_count", capture.mutationOutcomeCount)
|
|
1038
|
-
put("sealed_batch_count", capture.sealedBatchCount)
|
|
1039
|
-
put("rejected_mutation_count", capture.rejectedMutationCount)
|
|
1040
|
-
put("scope_state_count", capture.scopeStateCount)
|
|
1041
|
-
put("scope_row_count", capture.scopeRowCount)
|
|
1042
|
-
put("provenance_count", capture.provenanceCount)
|
|
1043
|
-
put("row_metadata_count", capture.rowMetadataCount)
|
|
1044
|
-
put("rebuild_attempt_count", capture.rebuildAttemptCount)
|
|
1045
|
-
put("rebuild_receipt_count", capture.rebuildReceiptCount)
|
|
1046
|
-
put(
|
|
1047
|
-
"provenance_maintenance_work_cursor",
|
|
1048
|
-
capture.provenanceMaintenanceWorkCursor.toString(),
|
|
1049
|
-
)
|
|
1050
|
-
}.toString())
|
|
1051
1137
|
} catch (error: Exception) {
|
|
1052
1138
|
rejectWithError(promise, error)
|
|
1053
1139
|
}
|
|
1054
1140
|
}
|
|
1055
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* Reads one record's row metadata and every rebuild receipt. The inspection snapshot bounds
|
|
1144
|
+
* row metadata, so a durable proof reads its identity directly.
|
|
1145
|
+
*/
|
|
1056
1146
|
@ReactMethod
|
|
1057
1147
|
override fun inspectDurableState(tableName: String, recordID: String, promise: Promise) {
|
|
1058
1148
|
val c = client ?: run {
|
|
@@ -1392,6 +1482,87 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1392
1482
|
})
|
|
1393
1483
|
}
|
|
1394
1484
|
|
|
1485
|
+
private fun clientStateJson(capture: ClientStateCaptureInspection): JSONObject {
|
|
1486
|
+
val schema: Any = capture.schema?.let {
|
|
1487
|
+
JSONObject().put("version", it.version).put("hash", it.hash)
|
|
1488
|
+
} ?: JSONObject.NULL
|
|
1489
|
+
val scopeStates = JSONArray(capture.scopeStates.map { value ->
|
|
1490
|
+
JSONObject().apply {
|
|
1491
|
+
put("scope_id", value.scopeID)
|
|
1492
|
+
put("cursor", value.cursor ?: JSONObject.NULL)
|
|
1493
|
+
put("checksum", value.checksum ?: JSONObject.NULL)
|
|
1494
|
+
put("local_checksum", value.localChecksum)
|
|
1495
|
+
put("generation", value.generation)
|
|
1496
|
+
}
|
|
1497
|
+
})
|
|
1498
|
+
val scopeRows = JSONArray(capture.scopeRows.map { value ->
|
|
1499
|
+
JSONObject().apply {
|
|
1500
|
+
put("scope_id", value.scopeID)
|
|
1501
|
+
put("table_name", value.tableName)
|
|
1502
|
+
put("record_id", value.recordID)
|
|
1503
|
+
put("checksum", value.checksum)
|
|
1504
|
+
put("generation", value.generation)
|
|
1505
|
+
}
|
|
1506
|
+
})
|
|
1507
|
+
val attempts = JSONArray(capture.rebuildAttempts.map { value ->
|
|
1508
|
+
JSONObject().apply {
|
|
1509
|
+
put("scope_id", value.scopeID)
|
|
1510
|
+
put("rebuild_id", value.rebuildID)
|
|
1511
|
+
put("client_generation", value.clientGeneration)
|
|
1512
|
+
put("schema_version", value.schemaVersion)
|
|
1513
|
+
put("schema_hash", value.schemaHash)
|
|
1514
|
+
put("generation", value.generation)
|
|
1515
|
+
put("cursor", value.cursor ?: JSONObject.NULL)
|
|
1516
|
+
put("page_limit", value.pageLimit)
|
|
1517
|
+
}
|
|
1518
|
+
})
|
|
1519
|
+
return JSONObject().apply {
|
|
1520
|
+
put("schema", schema)
|
|
1521
|
+
put("scope_states", scopeStates)
|
|
1522
|
+
put("scope_rows", scopeRows)
|
|
1523
|
+
put("rebuild_attempts", attempts)
|
|
1524
|
+
put("application_row_count", capture.applicationRowCount)
|
|
1525
|
+
put("mutation_ledger_count", capture.mutationLedgerCount)
|
|
1526
|
+
put("mutation_outcome_count", capture.mutationOutcomeCount)
|
|
1527
|
+
put("sealed_batch_count", capture.sealedBatchCount)
|
|
1528
|
+
put("rejected_mutation_count", capture.rejectedMutationCount)
|
|
1529
|
+
put("scope_state_count", capture.scopeStateCount)
|
|
1530
|
+
put("scope_row_count", capture.scopeRowCount)
|
|
1531
|
+
put("provenance_count", capture.provenanceCount)
|
|
1532
|
+
put("row_metadata_count", capture.rowMetadataCount)
|
|
1533
|
+
put("rebuild_attempt_count", capture.rebuildAttemptCount)
|
|
1534
|
+
put("rebuild_receipt_count", capture.rebuildReceiptCount)
|
|
1535
|
+
put(
|
|
1536
|
+
"provenance_maintenance_work_cursor",
|
|
1537
|
+
capture.provenanceMaintenanceWorkCursor.toString(),
|
|
1538
|
+
)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private fun parseStatements(statements: ReadableArray): List<SQLStatement> =
|
|
1543
|
+
(0 until statements.size()).map { i ->
|
|
1544
|
+
val item = statements.getMap(i) ?: throw IllegalArgumentException("Invalid SQL statement at index $i")
|
|
1545
|
+
val sql = item.getString("sql") ?: throw IllegalArgumentException("Missing SQL at index $i")
|
|
1546
|
+
val params = if (item.hasKey("params")) item.getArray("params") else null
|
|
1547
|
+
SQLStatement(sql, params?.let { parseParams(it) } ?: emptyArray())
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private fun retainedMutationJson(value: RetainedMutationInspection): JSONObject = when (value) {
|
|
1551
|
+
is RetainedMutationInspection.Current -> pendingMutationJson(value.mutation).put("representation", "current")
|
|
1552
|
+
is RetainedMutationInspection.Legacy -> JSONObject().apply {
|
|
1553
|
+
put("representation", "legacy")
|
|
1554
|
+
put("mutationID", value.mutation.mutationID)
|
|
1555
|
+
put("localOrder", value.mutation.localOrder)
|
|
1556
|
+
put("tableName", value.mutation.tableName)
|
|
1557
|
+
put("recordID", value.mutation.recordID)
|
|
1558
|
+
put("operation", operationWireValue(value.mutation.operation))
|
|
1559
|
+
put("baseVersion", value.mutation.baseVersion ?: JSONObject.NULL)
|
|
1560
|
+
put("clientVersion", value.mutation.clientVersion)
|
|
1561
|
+
put("status", localMutationStatusWireValue(value.mutation.status))
|
|
1562
|
+
put("sourceKind", value.mutation.sourceKind)
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1395
1566
|
private fun pendingMutationJson(mutation: PendingMutationInspection): JSONObject = JSONObject().apply {
|
|
1396
1567
|
put("mutationID", mutation.mutationID)
|
|
1397
1568
|
put("localOrder", mutation.localOrder)
|
|
@@ -1422,6 +1593,23 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1422
1593
|
}))
|
|
1423
1594
|
}
|
|
1424
1595
|
|
|
1596
|
+
private fun retainedRejectionJson(value: RetainedRejectionInspection): JSONObject = when (value) {
|
|
1597
|
+
is RetainedRejectionInspection.Current -> rejectedMutationJson(value.rejection).put("representation", "current")
|
|
1598
|
+
is RetainedRejectionInspection.Legacy -> JSONObject().apply {
|
|
1599
|
+
put("representation", "legacy")
|
|
1600
|
+
put("mutationID", value.rejection.mutationID)
|
|
1601
|
+
put("tableName", value.rejection.tableName)
|
|
1602
|
+
put("recordID", value.rejection.recordID)
|
|
1603
|
+
put("status", mutationStatusWireValue(value.rejection.status))
|
|
1604
|
+
put("code", mutationRejectionCodeWireValue(value.rejection.code))
|
|
1605
|
+
put("message", value.rejection.message ?: JSONObject.NULL)
|
|
1606
|
+
put("serverRowJSON", value.rejection.serverRowJSON ?: JSONObject.NULL)
|
|
1607
|
+
put("serverVersion", value.rejection.serverVersion ?: JSONObject.NULL)
|
|
1608
|
+
put("createdAt", value.rejection.createdAt)
|
|
1609
|
+
put("updatedAt", value.rejection.updatedAt)
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1425
1613
|
private fun rejectedMutationJson(mutation: RejectedMutationInspection): JSONObject = JSONObject().apply {
|
|
1426
1614
|
put("mutationID", mutation.mutationID)
|
|
1427
1615
|
put("tableName", mutation.tableName)
|
|
@@ -1476,6 +1664,14 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1476
1664
|
MutationRejectionCode.POLICY_REJECTED -> "policy_rejected"
|
|
1477
1665
|
MutationRejectionCode.VALIDATION_FAILED -> "validation_failed"
|
|
1478
1666
|
MutationRejectionCode.TABLE_NOT_SYNCED -> "table_not_synced"
|
|
1667
|
+
MutationRejectionCode.ATOMIC_BATCH_REJECTED -> "atomic_batch_rejected"
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
private fun atomicGroupInvalidReasonWireValue(reason: AtomicGroupInvalidReason): String = when (reason) {
|
|
1671
|
+
AtomicGroupInvalidReason.DELETE_FOLLOWED_BY_WRITE -> "deleteFollowedByWrite"
|
|
1672
|
+
AtomicGroupInvalidReason.TOO_MANY_MUTATIONS -> "tooManyMutations"
|
|
1673
|
+
AtomicGroupInvalidReason.MUTATION_TOO_LARGE -> "mutationTooLarge"
|
|
1674
|
+
AtomicGroupInvalidReason.REQUEST_TOO_LARGE -> "requestTooLarge"
|
|
1479
1675
|
}
|
|
1480
1676
|
|
|
1481
1677
|
private fun jsonObjectToJsonObject(value: JsonObject): JSONObject {
|