@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.
- package/README.md +112 -7
- package/SynchroReactNative.podspec +18 -1
- package/android/build.gradle +5 -1
- package/android/src/androidTest/kotlin/com/trainstar/synchro/rn/SynchroModuleTransactionTest.kt +593 -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 +328 -112
- package/ios/SynchroModule.mm +27 -4
- package/ios/SynchroModule.swift +251 -89
- package/ios-test-host/main.m +48 -0
- package/ios-tests/SynchroModuleTransactionTests.swift +709 -0
- package/ios-tests/TransactionSessionTests.swift +66 -0
- package/lib/module/NativeSynchro.js.map +1 -1
- package/lib/module/SynchroClient.js +141 -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 +99 -8
- 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/e2e/schema-check.test.d.ts +2 -0
- package/lib/typescript/example/e2e/schema-check.test.d.ts.map +1 -0
- package/lib/typescript/example/src/conformance/runner.d.ts +17 -2
- 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 +23 -5
- package/lib/typescript/src/inspection.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +62 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +21 -6
- package/src/NativeSynchro.ts +6 -1
- package/src/SynchroClient.ts +160 -1
- package/src/errors.ts +30 -0
- package/src/index.ts +7 -0
- package/src/inspection.ts +138 -9
- package/src/types.ts +70 -1
- package/tooling-patches/apply.cjs +163 -0
- package/tooling-patches/cli-clean-20.2.0.patch +21 -0
- package/tooling-patches/cli-config-android-20.2.0.patch +13 -0
- package/tooling-patches/react-native-builder-bob-0.40.18.patch +74 -0
- package/tooling-patches/stream-json-1.9.1.patch +40 -0
|
@@ -8,10 +8,14 @@ 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
|
|
14
15
|
import com.trainstar.synchro.inspection.TransportOperationClass
|
|
16
|
+
import com.trainstar.synchro.inspection.MigrationCheckpoint
|
|
17
|
+
import com.trainstar.synchro.inspection.MigrationJournalInspection
|
|
18
|
+
import com.trainstar.synchro.inspection.PhysicalSchemaColumnInspection
|
|
15
19
|
import com.trainstar.synchro.inspection.withTransportObservation
|
|
16
20
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
17
21
|
import kotlinx.coroutines.*
|
|
@@ -50,11 +54,12 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
50
54
|
val INT64_PATTERN = Regex("^(?:0|-?[1-9][0-9]*)$")
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
internal var client: SynchroClient? = null
|
|
58
|
+
private set
|
|
54
59
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
55
60
|
private val lifecycleMutex = Mutex()
|
|
56
61
|
private val transactionLock = Any()
|
|
57
|
-
|
|
62
|
+
internal val sessions = ConcurrentHashMap<String, TransactionSession>()
|
|
58
63
|
private var acceptingTransactions = false
|
|
59
64
|
private val observers = ConcurrentHashMap<String, Cancellable>()
|
|
60
65
|
private val pendingAuthContinuations = ConcurrentHashMap<String, CancellableContinuation<String>>()
|
|
@@ -136,6 +141,9 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
136
141
|
)
|
|
137
142
|
is SynchroError.AlreadyStarted -> "ALREADY_STARTED" to emptyMap()
|
|
138
143
|
is SynchroError.NotStarted -> "NOT_STARTED" to emptyMap()
|
|
144
|
+
is SynchroError.AtomicGroupInvalid -> "atomic_group_invalid" to mapOf(
|
|
145
|
+
"reason" to atomicGroupInvalidReasonWireValue(error.reason)
|
|
146
|
+
)
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -404,13 +412,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
404
412
|
return
|
|
405
413
|
}
|
|
406
414
|
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)
|
|
415
|
+
val total = c.executeBatch(parseStatements(statements))
|
|
414
416
|
val map = Arguments.createMap().apply {
|
|
415
417
|
putInt("totalRowsAffected", total)
|
|
416
418
|
}
|
|
@@ -422,11 +424,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
422
424
|
|
|
423
425
|
// MARK: - Transactions
|
|
424
426
|
|
|
425
|
-
|
|
427
|
+
internal class TransactionSession(
|
|
428
|
+
val isWrite: Boolean,
|
|
429
|
+
private var beginSettlement: ((Result<String>) -> Unit)?,
|
|
430
|
+
) {
|
|
426
431
|
val operations = Channel<TransactionOp>(Channel.RENDEZVOUS)
|
|
427
432
|
lateinit var job: Job
|
|
428
433
|
private var abortCause: Throwable? = null
|
|
429
434
|
private var terminal = false
|
|
435
|
+
private var terminalCompletion: CompletableDeferred<Unit>? = null
|
|
436
|
+
|
|
437
|
+
// A cancelled receive can drop an operation that its sender already handed over.
|
|
438
|
+
// The session owns each submitted operation until its completion settles.
|
|
439
|
+
private val outstanding = mutableSetOf<TransactionOp>()
|
|
430
440
|
|
|
431
441
|
@Synchronized
|
|
432
442
|
fun abort(cause: Throwable) {
|
|
@@ -436,16 +446,106 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
436
446
|
operations.close(cause)
|
|
437
447
|
}
|
|
438
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Ends the session after its transaction returns. Each operation that the loop did not
|
|
451
|
+
* complete, received or not, settles with the abort cause. A rollback settles successfully.
|
|
452
|
+
*/
|
|
453
|
+
@Synchronized
|
|
454
|
+
fun end() {
|
|
455
|
+
if (!terminal) {
|
|
456
|
+
terminal = true
|
|
457
|
+
operations.close()
|
|
458
|
+
}
|
|
459
|
+
outstanding.toList().forEach(::settleUnrun)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Submits one operation. A session that already ended settles it at once. */
|
|
463
|
+
suspend fun <T> perform(op: TransactionOp, deferred: CompletableDeferred<T>): T {
|
|
464
|
+
if (submit(op)) {
|
|
465
|
+
try {
|
|
466
|
+
operations.send(op)
|
|
467
|
+
} catch (_: Exception) {
|
|
468
|
+
// The channel closed or dropped the operation. The session settles it when it ends.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return deferred.await()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@Synchronized
|
|
475
|
+
fun submit(op: TransactionOp): Boolean {
|
|
476
|
+
if (terminal) {
|
|
477
|
+
settleUnrun(op)
|
|
478
|
+
return false
|
|
479
|
+
}
|
|
480
|
+
outstanding += op
|
|
481
|
+
return true
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private fun settleUnrun(op: TransactionOp) {
|
|
485
|
+
outstanding -= op
|
|
486
|
+
when (op) {
|
|
487
|
+
is TransactionOp.Rollback -> op.deferred.complete(Unit)
|
|
488
|
+
else -> op.deferred.completeExceptionally(abortCause ?: TransactionAbortedException())
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun release(deferred: CompletableDeferred<*>) {
|
|
493
|
+
outstanding.removeAll { it.deferred === deferred }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Settles begin when the transaction callback starts.
|
|
498
|
+
* A close that aborted the session earlier rejects begin instead.
|
|
499
|
+
*/
|
|
500
|
+
@Synchronized
|
|
501
|
+
fun acceptBegin(txID: String) {
|
|
502
|
+
val settle = beginSettlement
|
|
503
|
+
beginSettlement = null
|
|
504
|
+
val cause = abortCause
|
|
505
|
+
if (cause != null) {
|
|
506
|
+
settle?.invoke(Result.failure(cause))
|
|
507
|
+
throw cause
|
|
508
|
+
}
|
|
509
|
+
settle?.invoke(Result.success(txID))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Rejects begin when the transaction fails before its callback starts. */
|
|
513
|
+
@Synchronized
|
|
514
|
+
fun rejectBegin(error: Throwable) {
|
|
515
|
+
val settle = beginSettlement ?: return
|
|
516
|
+
beginSettlement = null
|
|
517
|
+
settle(Result.failure(error))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Takes ownership of a received commit or rollback completion before the terminal decision.
|
|
522
|
+
* The job settles the owned completion after the database transaction ends.
|
|
523
|
+
*/
|
|
439
524
|
@Synchronized
|
|
440
|
-
fun
|
|
525
|
+
fun acceptTerminal(completion: CompletableDeferred<Unit>): Boolean {
|
|
526
|
+
release(completion)
|
|
527
|
+
terminalCompletion = completion
|
|
441
528
|
if (terminal) return false
|
|
442
529
|
terminal = true
|
|
443
530
|
operations.close()
|
|
444
531
|
return true
|
|
445
532
|
}
|
|
446
533
|
|
|
534
|
+
/** Settles the owned terminal completion after the database transaction ends. */
|
|
535
|
+
@Synchronized
|
|
536
|
+
fun finishTerminal(error: Throwable?) {
|
|
537
|
+
val completion = terminalCompletion ?: return
|
|
538
|
+
terminalCompletion = null
|
|
539
|
+
if (error == null) {
|
|
540
|
+
completion.complete(Unit)
|
|
541
|
+
} else {
|
|
542
|
+
completion.completeExceptionally(error)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
447
546
|
@Synchronized
|
|
448
547
|
fun <T> complete(deferred: CompletableDeferred<T>, value: T) {
|
|
548
|
+
release(deferred)
|
|
449
549
|
val cause = abortCause
|
|
450
550
|
if (cause == null) {
|
|
451
551
|
deferred.complete(value)
|
|
@@ -456,21 +556,22 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
456
556
|
|
|
457
557
|
@Synchronized
|
|
458
558
|
fun <T> completeExceptionally(deferred: CompletableDeferred<T>, error: Throwable) {
|
|
559
|
+
release(deferred)
|
|
459
560
|
deferred.completeExceptionally(abortCause ?: error)
|
|
460
561
|
}
|
|
461
562
|
}
|
|
462
563
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
class
|
|
468
|
-
class
|
|
564
|
+
internal sealed class TransactionOp {
|
|
565
|
+
abstract val deferred: CompletableDeferred<*>
|
|
566
|
+
|
|
567
|
+
class Query(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableArray>) : TransactionOp()
|
|
568
|
+
class QueryOne(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableMap?>) : TransactionOp()
|
|
569
|
+
class Execute(val sql: String, val params: Array<Any?>, override val deferred: CompletableDeferred<WritableMap>) : TransactionOp()
|
|
570
|
+
class Commit(override val deferred: CompletableDeferred<Unit>) : TransactionOp()
|
|
571
|
+
class Rollback(override val deferred: CompletableDeferred<Unit>) : TransactionOp()
|
|
469
572
|
}
|
|
470
573
|
|
|
471
|
-
|
|
472
|
-
val completion: CompletableDeferred<Unit>,
|
|
473
|
-
) : Exception("rollback")
|
|
574
|
+
internal class TransactionRollbackException : Exception("rollback")
|
|
474
575
|
|
|
475
576
|
private class TransactionAbortedException : Exception("Client closed during transaction")
|
|
476
577
|
private class TransactionExpiredException : Exception("Transaction timed out due to inactivity")
|
|
@@ -480,12 +581,17 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
480
581
|
beginTransaction(isWrite = true, promise = promise)
|
|
481
582
|
}
|
|
482
583
|
|
|
584
|
+
@ReactMethod
|
|
585
|
+
override fun beginAtomicWriteTransaction(promise: Promise) {
|
|
586
|
+
beginTransaction(isWrite = true, isAtomic = true, promise = promise)
|
|
587
|
+
}
|
|
588
|
+
|
|
483
589
|
@ReactMethod
|
|
484
590
|
override fun beginReadTransaction(promise: Promise) {
|
|
485
591
|
beginTransaction(isWrite = false, promise = promise)
|
|
486
592
|
}
|
|
487
593
|
|
|
488
|
-
private fun beginTransaction(isWrite: Boolean, promise: Promise) {
|
|
594
|
+
private fun beginTransaction(isWrite: Boolean, promise: Promise, isAtomic: Boolean = false) {
|
|
489
595
|
val c = synchronized(transactionLock) {
|
|
490
596
|
client.takeIf { acceptingTransactions }
|
|
491
597
|
} ?: run {
|
|
@@ -494,28 +600,31 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
494
600
|
}
|
|
495
601
|
|
|
496
602
|
val txID = UUID.randomUUID().toString()
|
|
497
|
-
val session = TransactionSession(isWrite)
|
|
603
|
+
val session = TransactionSession(isWrite) { result ->
|
|
604
|
+
result.fold(
|
|
605
|
+
onSuccess = { promise.resolve(it) },
|
|
606
|
+
onFailure = { rejectWithError(promise, it) },
|
|
607
|
+
)
|
|
608
|
+
}
|
|
498
609
|
|
|
499
610
|
val job = scope.launch(start = CoroutineStart.LAZY) {
|
|
500
|
-
var finalDeferred: CompletableDeferred<Unit>? = null
|
|
501
611
|
try {
|
|
502
612
|
if (isWrite) {
|
|
503
|
-
|
|
504
|
-
|
|
613
|
+
val body: (ApplicationTransaction) -> Unit = { transaction ->
|
|
614
|
+
runTransactionLoop(
|
|
505
615
|
txID = txID,
|
|
506
616
|
session = session,
|
|
507
|
-
promise = promise,
|
|
508
617
|
query = transaction::query,
|
|
509
618
|
queryOne = transaction::queryOne,
|
|
510
619
|
execute = transaction::execute,
|
|
511
620
|
)
|
|
512
621
|
}
|
|
622
|
+
if (isAtomic) c.atomicWriteTransaction(body) else c.writeTransaction(body)
|
|
513
623
|
} else {
|
|
514
624
|
c.readTransaction { transaction ->
|
|
515
|
-
|
|
625
|
+
runTransactionLoop(
|
|
516
626
|
txID = txID,
|
|
517
627
|
session = session,
|
|
518
|
-
promise = promise,
|
|
519
628
|
query = transaction::query,
|
|
520
629
|
queryOne = transaction::queryOne,
|
|
521
630
|
execute = { _, _ ->
|
|
@@ -524,17 +633,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
524
633
|
)
|
|
525
634
|
}
|
|
526
635
|
}
|
|
527
|
-
|
|
636
|
+
session.finishTerminal(null)
|
|
528
637
|
} catch (e: TimeoutCancellationException) {
|
|
529
638
|
val timeout = TransactionExpiredException()
|
|
530
639
|
session.abort(timeout)
|
|
531
|
-
|
|
640
|
+
session.finishTerminal(timeout)
|
|
532
641
|
} catch (e: TransactionRollbackException) {
|
|
533
|
-
|
|
642
|
+
session.finishTerminal(null)
|
|
534
643
|
} catch (e: Exception) {
|
|
535
|
-
|
|
644
|
+
// Acquisition can fail before the callback settles begin.
|
|
645
|
+
session.rejectBegin(e)
|
|
646
|
+
session.finishTerminal(e)
|
|
536
647
|
} finally {
|
|
537
|
-
session.
|
|
648
|
+
session.end()
|
|
538
649
|
sessions.remove(txID, session)
|
|
539
650
|
}
|
|
540
651
|
}
|
|
@@ -555,16 +666,14 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
555
666
|
job.start()
|
|
556
667
|
}
|
|
557
668
|
|
|
558
|
-
|
|
669
|
+
internal fun runTransactionLoop(
|
|
559
670
|
txID: String,
|
|
560
671
|
session: TransactionSession,
|
|
561
|
-
promise: Promise,
|
|
562
672
|
query: (String, Array<out Any?>?) -> List<Row>,
|
|
563
673
|
queryOne: (String, Array<out Any?>?) -> Row?,
|
|
564
674
|
execute: (String, Array<out Any?>?) -> ExecResult,
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
var finalDeferred: CompletableDeferred<Unit>? = null
|
|
675
|
+
) {
|
|
676
|
+
session.acceptBegin(txID)
|
|
568
677
|
runBlocking {
|
|
569
678
|
transactionLoop@ while (true) {
|
|
570
679
|
val result = withTimeout(5000) { session.operations.receiveCatching() }
|
|
@@ -604,19 +713,16 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
604
713
|
}
|
|
605
714
|
}
|
|
606
715
|
is TransactionOp.Commit -> {
|
|
607
|
-
|
|
608
|
-
if (!session.completeNormally()) throw TransactionAbortedException()
|
|
716
|
+
if (!session.acceptTerminal(op.deferred)) throw TransactionAbortedException()
|
|
609
717
|
break@transactionLoop
|
|
610
718
|
}
|
|
611
719
|
is TransactionOp.Rollback -> {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
throw TransactionRollbackException(op.deferred)
|
|
720
|
+
if (!session.acceptTerminal(op.deferred)) throw TransactionAbortedException()
|
|
721
|
+
throw TransactionRollbackException()
|
|
615
722
|
}
|
|
616
723
|
}
|
|
617
724
|
}
|
|
618
725
|
}
|
|
619
|
-
return finalDeferred
|
|
620
726
|
}
|
|
621
727
|
|
|
622
728
|
@ReactMethod
|
|
@@ -628,8 +734,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
628
734
|
scope.launch {
|
|
629
735
|
try {
|
|
630
736
|
val deferred = CompletableDeferred<WritableArray>()
|
|
631
|
-
session.
|
|
632
|
-
promise.resolve(deferred.await())
|
|
737
|
+
promise.resolve(session.perform(TransactionOp.Query(sql, parseParams(params), deferred), deferred))
|
|
633
738
|
} catch (e: Exception) {
|
|
634
739
|
rejectWithError(promise, e)
|
|
635
740
|
}
|
|
@@ -645,8 +750,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
645
750
|
scope.launch {
|
|
646
751
|
try {
|
|
647
752
|
val deferred = CompletableDeferred<WritableMap?>()
|
|
648
|
-
session.
|
|
649
|
-
promise.resolve(deferred.await())
|
|
753
|
+
promise.resolve(session.perform(TransactionOp.QueryOne(sql, parseParams(params), deferred), deferred))
|
|
650
754
|
} catch (e: Exception) {
|
|
651
755
|
rejectWithError(promise, e)
|
|
652
756
|
}
|
|
@@ -666,8 +770,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
666
770
|
scope.launch {
|
|
667
771
|
try {
|
|
668
772
|
val deferred = CompletableDeferred<WritableMap>()
|
|
669
|
-
session.
|
|
670
|
-
promise.resolve(deferred.await())
|
|
773
|
+
promise.resolve(session.perform(TransactionOp.Execute(sql, parseParams(params), deferred), deferred))
|
|
671
774
|
} catch (e: Exception) {
|
|
672
775
|
rejectWithError(promise, e)
|
|
673
776
|
}
|
|
@@ -683,8 +786,7 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
683
786
|
scope.launch {
|
|
684
787
|
try {
|
|
685
788
|
val deferred = CompletableDeferred<Unit>()
|
|
686
|
-
session.
|
|
687
|
-
deferred.await()
|
|
789
|
+
session.perform(TransactionOp.Commit(deferred), deferred)
|
|
688
790
|
promise.resolve(null)
|
|
689
791
|
} catch (e: Exception) {
|
|
690
792
|
rejectWithError(promise, e)
|
|
@@ -700,12 +802,12 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
700
802
|
}
|
|
701
803
|
scope.launch {
|
|
702
804
|
try {
|
|
805
|
+
// A session that ends before it runs this rollback settles it successfully.
|
|
703
806
|
val deferred = CompletableDeferred<Unit>()
|
|
704
|
-
session.
|
|
705
|
-
deferred.await()
|
|
807
|
+
session.perform(TransactionOp.Rollback(deferred), deferred)
|
|
706
808
|
promise.resolve(null)
|
|
707
809
|
} catch (e: Exception) {
|
|
708
|
-
promise
|
|
810
|
+
rejectWithError(promise, e)
|
|
709
811
|
}
|
|
710
812
|
}
|
|
711
813
|
}
|
|
@@ -972,6 +1074,19 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
972
1074
|
}
|
|
973
1075
|
}
|
|
974
1076
|
|
|
1077
|
+
@ReactMethod
|
|
1078
|
+
override fun inspectRetainedMutationRecords(promise: Promise) {
|
|
1079
|
+
val c = client ?: run {
|
|
1080
|
+
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
promise.resolve(JSONArray(c.inspectRetainedMutationRecords().map(::retainedMutationJson)).toString())
|
|
1085
|
+
} catch (e: Exception) {
|
|
1086
|
+
rejectWithError(promise, e)
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
975
1090
|
@ReactMethod
|
|
976
1091
|
override fun inspectRejectedMutations(promise: Promise) {
|
|
977
1092
|
val c = client ?: run {
|
|
@@ -986,73 +1101,61 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
986
1101
|
}
|
|
987
1102
|
|
|
988
1103
|
@ReactMethod
|
|
989
|
-
override fun
|
|
1104
|
+
override fun inspectRejectedMutationRecords(promise: Promise) {
|
|
990
1105
|
val c = client ?: run {
|
|
991
1106
|
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
992
1107
|
return
|
|
993
1108
|
}
|
|
994
1109
|
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)
|
|
1110
|
+
promise.resolve(JSONArray(c.inspectRejectedMutationRecords().map(::retainedRejectionJson)).toString())
|
|
1111
|
+
} catch (e: Exception) {
|
|
1112
|
+
rejectWithError(promise, e)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/** Reads client state, retained details, and the requested application rows from one read-only snapshot. */
|
|
1117
|
+
@ReactMethod
|
|
1118
|
+
override fun inspectClientStateSnapshot(rowStatements: ReadableArray, promise: Promise) {
|
|
1119
|
+
val c = client ?: run {
|
|
1120
|
+
promise.reject("NOT_CONNECTED", "Client not initialized")
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
val statements = parseStatements(rowStatements)
|
|
1125
|
+
val rows = mutableListOf<Row>()
|
|
1126
|
+
val snapshot = SynchroInspection(c).captureSnapshot(maximumRecords = 512) { _, transaction ->
|
|
1127
|
+
for (statement in statements) {
|
|
1128
|
+
rows += transaction.query(statement.sql, statement.params)
|
|
1028
1129
|
}
|
|
1130
|
+
}
|
|
1131
|
+
val inspection = JSONObject().apply {
|
|
1132
|
+
put("client_state", clientStateJson(snapshot.capture))
|
|
1133
|
+
put("migration_journal", snapshot.capture.migrationJournal?.let {
|
|
1134
|
+
JSONObject(Json.encodeToString(MigrationJournalInspection.serializer(), it))
|
|
1135
|
+
} ?: JSONObject.NULL)
|
|
1136
|
+
put("migration_journal_truncated", snapshot.capture.migrationJournalTruncated)
|
|
1137
|
+
put("physical_schema", JSONArray(snapshot.capture.physicalSchema.map {
|
|
1138
|
+
JSONObject(Json.encodeToString(PhysicalSchemaColumnInspection.serializer(), it))
|
|
1139
|
+
}))
|
|
1140
|
+
put("physical_schema_truncated", snapshot.capture.physicalSchemaTruncated)
|
|
1141
|
+
put("accepted_mutation_outcomes", JSONObject(snapshot.capture.acceptedMutationOutcomes))
|
|
1142
|
+
put("accepted_mutation_outcomes_truncated", snapshot.capture.acceptedMutationOutcomesTruncated)
|
|
1143
|
+
put("retained_mutations", snapshot.retainedMutations?.let { JSONArray(it.map(::retainedMutationJson)) } ?: JSONObject.NULL)
|
|
1144
|
+
put("rejected_mutations", snapshot.rejectedMutations?.let { JSONArray(it.map(::retainedRejectionJson)) } ?: JSONObject.NULL)
|
|
1145
|
+
}
|
|
1146
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1147
|
+
putString("inspection", inspection.toString())
|
|
1148
|
+
putArray("applicationRows", rowsToWritableArray(rows))
|
|
1029
1149
|
})
|
|
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
1150
|
} catch (error: Exception) {
|
|
1052
1151
|
rejectWithError(promise, error)
|
|
1053
1152
|
}
|
|
1054
1153
|
}
|
|
1055
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* Reads one record's row metadata and every rebuild receipt. The inspection snapshot bounds
|
|
1157
|
+
* row metadata, so a durable proof reads its identity directly.
|
|
1158
|
+
*/
|
|
1056
1159
|
@ReactMethod
|
|
1057
1160
|
override fun inspectDurableState(tableName: String, recordID: String, promise: Promise) {
|
|
1058
1161
|
val c = client ?: run {
|
|
@@ -1112,7 +1215,8 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1112
1215
|
override fun armTransportPause(operationClass: String, promise: Promise) {
|
|
1113
1216
|
try {
|
|
1114
1217
|
val collector = transportObservationCollector ?: error("Transport observation is not configured")
|
|
1115
|
-
|
|
1218
|
+
val checkpoint = MigrationCheckpoint.fromWire(operationClass)
|
|
1219
|
+
if (checkpoint != null) collector.armPause(checkpoint) else collector.armPause(transportOperationClass(operationClass))
|
|
1116
1220
|
promise.resolve(null)
|
|
1117
1221
|
} catch (error: Exception) {
|
|
1118
1222
|
rejectWithError(promise, error)
|
|
@@ -1130,7 +1234,12 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1130
1234
|
timeoutMs % 1.0 == 0.0,
|
|
1131
1235
|
) { "Transport pause timeout is invalid" }
|
|
1132
1236
|
val collector = transportObservationCollector ?: error("Transport observation is not configured")
|
|
1133
|
-
|
|
1237
|
+
val checkpoint = MigrationCheckpoint.fromWire(operationClass)
|
|
1238
|
+
if (checkpoint != null) {
|
|
1239
|
+
collector.awaitPause(checkpoint, timeoutMs.toLong())
|
|
1240
|
+
} else {
|
|
1241
|
+
collector.awaitPause(transportOperationClass(operationClass), timeoutMs.toLong())
|
|
1242
|
+
}
|
|
1134
1243
|
promise.resolve(null)
|
|
1135
1244
|
} catch (error: Exception) {
|
|
1136
1245
|
rejectWithError(promise, error)
|
|
@@ -1392,6 +1501,88 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1392
1501
|
})
|
|
1393
1502
|
}
|
|
1394
1503
|
|
|
1504
|
+
private fun clientStateJson(capture: ClientStateCaptureInspection): JSONObject {
|
|
1505
|
+
val schema: Any = capture.schema?.let {
|
|
1506
|
+
JSONObject().put("version", it.version).put("hash", it.hash)
|
|
1507
|
+
} ?: JSONObject.NULL
|
|
1508
|
+
val scopeStates = JSONArray(capture.scopeStates.map { value ->
|
|
1509
|
+
JSONObject().apply {
|
|
1510
|
+
put("scope_id", value.scopeID)
|
|
1511
|
+
put("cursor", value.cursor ?: JSONObject.NULL)
|
|
1512
|
+
put("checksum", value.checksum ?: JSONObject.NULL)
|
|
1513
|
+
put("local_checksum", value.localChecksum)
|
|
1514
|
+
put("generation", value.generation)
|
|
1515
|
+
}
|
|
1516
|
+
})
|
|
1517
|
+
val scopeRows = JSONArray(capture.scopeRows.map { value ->
|
|
1518
|
+
JSONObject().apply {
|
|
1519
|
+
put("scope_id", value.scopeID)
|
|
1520
|
+
put("table_name", value.tableName)
|
|
1521
|
+
put("record_id", value.recordID)
|
|
1522
|
+
put("checksum", value.checksum)
|
|
1523
|
+
put("generation", value.generation)
|
|
1524
|
+
}
|
|
1525
|
+
})
|
|
1526
|
+
val attempts = JSONArray(capture.rebuildAttempts.map { value ->
|
|
1527
|
+
JSONObject().apply {
|
|
1528
|
+
put("scope_id", value.scopeID)
|
|
1529
|
+
put("rebuild_id", value.rebuildID)
|
|
1530
|
+
put("client_generation", value.clientGeneration)
|
|
1531
|
+
put("schema_version", value.schemaVersion)
|
|
1532
|
+
put("schema_hash", value.schemaHash)
|
|
1533
|
+
put("generation", value.generation)
|
|
1534
|
+
put("cursor", value.cursor ?: JSONObject.NULL)
|
|
1535
|
+
put("page_limit", value.pageLimit)
|
|
1536
|
+
}
|
|
1537
|
+
})
|
|
1538
|
+
return JSONObject().apply {
|
|
1539
|
+
put("schema", schema)
|
|
1540
|
+
put("scope_states", scopeStates)
|
|
1541
|
+
put("scope_rows", scopeRows)
|
|
1542
|
+
put("rebuild_attempts", attempts)
|
|
1543
|
+
put("application_row_count", capture.applicationRowCount)
|
|
1544
|
+
put("mutation_ledger_count", capture.mutationLedgerCount)
|
|
1545
|
+
put("mutation_outcome_count", capture.mutationOutcomeCount)
|
|
1546
|
+
put("sealed_batch_count", capture.sealedBatchCount)
|
|
1547
|
+
put("rejected_mutation_count", capture.rejectedMutationCount)
|
|
1548
|
+
put("scope_state_count", capture.scopeStateCount)
|
|
1549
|
+
put("scope_row_count", capture.scopeRowCount)
|
|
1550
|
+
put("provenance_count", capture.provenanceCount)
|
|
1551
|
+
put("row_metadata_count", capture.rowMetadataCount)
|
|
1552
|
+
put("rebuild_attempt_count", capture.rebuildAttemptCount)
|
|
1553
|
+
put("rebuild_receipt_count", capture.rebuildReceiptCount)
|
|
1554
|
+
put("capture_overflowed", capture.overflowed)
|
|
1555
|
+
put(
|
|
1556
|
+
"provenance_maintenance_work_cursor",
|
|
1557
|
+
capture.provenanceMaintenanceWorkCursor.toString(),
|
|
1558
|
+
)
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
private fun parseStatements(statements: ReadableArray): List<SQLStatement> =
|
|
1563
|
+
(0 until statements.size()).map { i ->
|
|
1564
|
+
val item = statements.getMap(i) ?: throw IllegalArgumentException("Invalid SQL statement at index $i")
|
|
1565
|
+
val sql = item.getString("sql") ?: throw IllegalArgumentException("Missing SQL at index $i")
|
|
1566
|
+
val params = if (item.hasKey("params")) item.getArray("params") else null
|
|
1567
|
+
SQLStatement(sql, params?.let { parseParams(it) } ?: emptyArray())
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private fun retainedMutationJson(value: RetainedMutationInspection): JSONObject = when (value) {
|
|
1571
|
+
is RetainedMutationInspection.Current -> pendingMutationJson(value.mutation).put("representation", "current")
|
|
1572
|
+
is RetainedMutationInspection.Legacy -> JSONObject().apply {
|
|
1573
|
+
put("representation", "legacy")
|
|
1574
|
+
put("mutationID", value.mutation.mutationID)
|
|
1575
|
+
put("localOrder", value.mutation.localOrder)
|
|
1576
|
+
put("tableName", value.mutation.tableName)
|
|
1577
|
+
put("recordID", value.mutation.recordID)
|
|
1578
|
+
put("operation", operationWireValue(value.mutation.operation))
|
|
1579
|
+
put("baseVersion", value.mutation.baseVersion ?: JSONObject.NULL)
|
|
1580
|
+
put("clientVersion", value.mutation.clientVersion)
|
|
1581
|
+
put("status", localMutationStatusWireValue(value.mutation.status))
|
|
1582
|
+
put("sourceKind", value.mutation.sourceKind)
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1395
1586
|
private fun pendingMutationJson(mutation: PendingMutationInspection): JSONObject = JSONObject().apply {
|
|
1396
1587
|
put("mutationID", mutation.mutationID)
|
|
1397
1588
|
put("localOrder", mutation.localOrder)
|
|
@@ -1422,6 +1613,23 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1422
1613
|
}))
|
|
1423
1614
|
}
|
|
1424
1615
|
|
|
1616
|
+
private fun retainedRejectionJson(value: RetainedRejectionInspection): JSONObject = when (value) {
|
|
1617
|
+
is RetainedRejectionInspection.Current -> rejectedMutationJson(value.rejection).put("representation", "current")
|
|
1618
|
+
is RetainedRejectionInspection.Legacy -> JSONObject().apply {
|
|
1619
|
+
put("representation", "legacy")
|
|
1620
|
+
put("mutationID", value.rejection.mutationID)
|
|
1621
|
+
put("tableName", value.rejection.tableName)
|
|
1622
|
+
put("recordID", value.rejection.recordID)
|
|
1623
|
+
put("status", mutationStatusWireValue(value.rejection.status))
|
|
1624
|
+
put("code", mutationRejectionCodeWireValue(value.rejection.code))
|
|
1625
|
+
put("message", value.rejection.message ?: JSONObject.NULL)
|
|
1626
|
+
put("serverRowJSON", value.rejection.serverRowJSON ?: JSONObject.NULL)
|
|
1627
|
+
put("serverVersion", value.rejection.serverVersion ?: JSONObject.NULL)
|
|
1628
|
+
put("createdAt", value.rejection.createdAt)
|
|
1629
|
+
put("updatedAt", value.rejection.updatedAt)
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1425
1633
|
private fun rejectedMutationJson(mutation: RejectedMutationInspection): JSONObject = JSONObject().apply {
|
|
1426
1634
|
put("mutationID", mutation.mutationID)
|
|
1427
1635
|
put("tableName", mutation.tableName)
|
|
@@ -1476,6 +1684,14 @@ class SynchroModule(reactContext: ReactApplicationContext) :
|
|
|
1476
1684
|
MutationRejectionCode.POLICY_REJECTED -> "policy_rejected"
|
|
1477
1685
|
MutationRejectionCode.VALIDATION_FAILED -> "validation_failed"
|
|
1478
1686
|
MutationRejectionCode.TABLE_NOT_SYNCED -> "table_not_synced"
|
|
1687
|
+
MutationRejectionCode.ATOMIC_BATCH_REJECTED -> "atomic_batch_rejected"
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
private fun atomicGroupInvalidReasonWireValue(reason: AtomicGroupInvalidReason): String = when (reason) {
|
|
1691
|
+
AtomicGroupInvalidReason.DELETE_FOLLOWED_BY_WRITE -> "deleteFollowedByWrite"
|
|
1692
|
+
AtomicGroupInvalidReason.TOO_MANY_MUTATIONS -> "tooManyMutations"
|
|
1693
|
+
AtomicGroupInvalidReason.MUTATION_TOO_LARGE -> "mutationTooLarge"
|
|
1694
|
+
AtomicGroupInvalidReason.REQUEST_TOO_LARGE -> "requestTooLarge"
|
|
1479
1695
|
}
|
|
1480
1696
|
|
|
1481
1697
|
private fun jsonObjectToJsonObject(value: JsonObject): JSONObject {
|