@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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
package com.trainstar.synchro.rn
|
|
2
|
+
|
|
3
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
4
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
5
|
+
import com.trainstar.synchro.ExecResult
|
|
6
|
+
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import java.util.concurrent.Callable
|
|
8
|
+
import java.util.concurrent.CountDownLatch
|
|
9
|
+
import java.util.concurrent.ExecutionException
|
|
10
|
+
import java.util.concurrent.Executors
|
|
11
|
+
import java.util.concurrent.TimeUnit
|
|
12
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
13
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
14
|
+
import kotlinx.coroutines.CompletableDeferred
|
|
15
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
16
|
+
import kotlinx.coroutines.async
|
|
17
|
+
import kotlinx.coroutines.asCoroutineDispatcher
|
|
18
|
+
import kotlinx.coroutines.channels.ChannelResult
|
|
19
|
+
import kotlinx.coroutines.runBlocking
|
|
20
|
+
import kotlinx.coroutines.withTimeout
|
|
21
|
+
import org.junit.Assert.assertEquals
|
|
22
|
+
import org.junit.Assert.assertFalse
|
|
23
|
+
import org.junit.Assert.assertSame
|
|
24
|
+
import org.junit.Assert.assertThrows
|
|
25
|
+
import org.junit.Assert.assertTrue
|
|
26
|
+
import org.junit.Test
|
|
27
|
+
import org.junit.runner.RunWith
|
|
28
|
+
|
|
29
|
+
private const val GUARD_SECONDS = 60L
|
|
30
|
+
|
|
31
|
+
private class SessionError(name: String) : Exception(name)
|
|
32
|
+
|
|
33
|
+
/** Records the begin settlements that the actual session delivers. */
|
|
34
|
+
private class BeginRecorder {
|
|
35
|
+
private val lock = Any()
|
|
36
|
+
private val results = mutableListOf<Result<String>>()
|
|
37
|
+
|
|
38
|
+
fun record(result: Result<String>) {
|
|
39
|
+
synchronized(lock) { results += result }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
val successes: List<String> get() = synchronized(lock) { results.mapNotNull { it.getOrNull() } }
|
|
43
|
+
val failures: List<Throwable> get() = synchronized(lock) { results.mapNotNull { it.exceptionOrNull() } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A received terminal completion and a count of its observable completion deliveries. */
|
|
47
|
+
private class ReceivedTerminal {
|
|
48
|
+
val deferred = CompletableDeferred<Unit>()
|
|
49
|
+
private val deliveries = AtomicInteger()
|
|
50
|
+
|
|
51
|
+
init {
|
|
52
|
+
deferred.invokeOnCompletion { deliveries.incrementAndGet() }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
val deliveryCount: Int get() = deliveries.get()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Direct tests of the production TransactionSession and one terminal-only run of the actual loop.
|
|
60
|
+
* Most tests select the order of the session calls. The dropped-operation test forces a physical
|
|
61
|
+
* Channel schedule.
|
|
62
|
+
*/
|
|
63
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
64
|
+
@RunWith(AndroidJUnit4::class)
|
|
65
|
+
class TransactionSessionTest {
|
|
66
|
+
@Test
|
|
67
|
+
fun failureBeforeCallbackRejectsStoredBeginOnce() {
|
|
68
|
+
val begin = BeginRecorder()
|
|
69
|
+
val session = SynchroModule.TransactionSession(isWrite = true, beginSettlement = begin::record)
|
|
70
|
+
val acquisition = SessionError("acquisition")
|
|
71
|
+
|
|
72
|
+
session.rejectBegin(acquisition)
|
|
73
|
+
session.rejectBegin(SessionError("later failure"))
|
|
74
|
+
|
|
75
|
+
assertEquals(emptyList<String>(), begin.successes)
|
|
76
|
+
assertEquals(1, begin.failures.size)
|
|
77
|
+
assertSame(acquisition, begin.failures.single())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test
|
|
81
|
+
fun abortBeforeBeginAcceptanceRejectsBeginAndUnwindsCallback() {
|
|
82
|
+
val begin = BeginRecorder()
|
|
83
|
+
val session = SynchroModule.TransactionSession(isWrite = true, beginSettlement = begin::record)
|
|
84
|
+
val close = SessionError("close")
|
|
85
|
+
|
|
86
|
+
session.abort(close)
|
|
87
|
+
val thrown = assertThrows(SessionError::class.java) { session.acceptBegin("abort-before-begin") }
|
|
88
|
+
session.rejectBegin(SessionError("callback exit"))
|
|
89
|
+
|
|
90
|
+
assertSame(close, thrown)
|
|
91
|
+
assertEquals(emptyList<String>(), begin.successes)
|
|
92
|
+
assertEquals(1, begin.failures.size)
|
|
93
|
+
assertSame(close, begin.failures.single())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun acceptedBeginIsNotSettledAgainByLaterFailure() {
|
|
98
|
+
val begin = BeginRecorder()
|
|
99
|
+
val session = SynchroModule.TransactionSession(isWrite = true, beginSettlement = begin::record)
|
|
100
|
+
|
|
101
|
+
session.acceptBegin("accepted")
|
|
102
|
+
session.abort(SessionError("close"))
|
|
103
|
+
session.rejectBegin(SessionError("callback exit"))
|
|
104
|
+
|
|
105
|
+
assertEquals(listOf("accepted"), begin.successes)
|
|
106
|
+
assertEquals(emptyList<Throwable>(), begin.failures)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The exact lost-owner order: close wins after the loop received a terminal operation. */
|
|
110
|
+
@Test
|
|
111
|
+
fun abortBeforeAcceptanceKeepsReceivedTerminalUntilFinish() {
|
|
112
|
+
val session = acceptedSession()
|
|
113
|
+
val received = ReceivedTerminal()
|
|
114
|
+
val exit = SessionError("transaction exit")
|
|
115
|
+
|
|
116
|
+
session.abort(SessionError("close"))
|
|
117
|
+
assertFalse(session.acceptTerminal(received.deferred))
|
|
118
|
+
assertFalse(received.deferred.isCompleted)
|
|
119
|
+
assertEquals(0, received.deliveryCount)
|
|
120
|
+
|
|
121
|
+
session.finishTerminal(exit)
|
|
122
|
+
assertTrue("received terminal operation lost its owner", received.deferred.isCompleted)
|
|
123
|
+
assertSame(exit, received.deferred.getCompletionExceptionOrNull())
|
|
124
|
+
assertEquals(1, received.deliveryCount)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun acceptanceBeforeAbortKeepsTerminalUntilSuccessfulFinish() {
|
|
129
|
+
val session = acceptedSession()
|
|
130
|
+
val received = ReceivedTerminal()
|
|
131
|
+
|
|
132
|
+
assertTrue(session.acceptTerminal(received.deferred))
|
|
133
|
+
session.abort(SessionError("close"))
|
|
134
|
+
assertFalse(received.deferred.isCompleted)
|
|
135
|
+
assertEquals(0, received.deliveryCount)
|
|
136
|
+
|
|
137
|
+
session.finishTerminal(null)
|
|
138
|
+
assertTrue(received.deferred.isCompleted)
|
|
139
|
+
assertEquals(null, received.deferred.getCompletionExceptionOrNull())
|
|
140
|
+
assertEquals(1, received.deliveryCount)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Test
|
|
144
|
+
fun acceptedTerminalRejectsWithTransactionExitError() {
|
|
145
|
+
val session = acceptedSession()
|
|
146
|
+
val received = ReceivedTerminal()
|
|
147
|
+
val exit = SessionError("transaction exit")
|
|
148
|
+
|
|
149
|
+
assertTrue(session.acceptTerminal(received.deferred))
|
|
150
|
+
session.finishTerminal(exit)
|
|
151
|
+
|
|
152
|
+
assertTrue(received.deferred.isCompleted)
|
|
153
|
+
assertSame(exit, received.deferred.getCompletionExceptionOrNull())
|
|
154
|
+
assertEquals(1, received.deliveryCount)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@Test
|
|
158
|
+
fun commitLoopTransfersReceivedDeferredToSession() {
|
|
159
|
+
val result = runTerminalLoop(commit = true)
|
|
160
|
+
|
|
161
|
+
assertEquals(null, result.loopFailure)
|
|
162
|
+
result.requireOwnedUntilFinish()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun rollbackLoopTransfersReceivedDeferredToSession() {
|
|
167
|
+
val result = runTerminalLoop(commit = false)
|
|
168
|
+
|
|
169
|
+
assertTrue(
|
|
170
|
+
"rollback loop did not exit with its control exception: ${result.loopFailure}",
|
|
171
|
+
result.loopFailure is SynchroModule.TransactionRollbackException,
|
|
172
|
+
)
|
|
173
|
+
result.requireOwnedUntilFinish()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Forces the Channel prompt-cancellation schedule on the actual session channel. The sender
|
|
178
|
+
* hands the operation to a waiting receive. The receive is cancelled, as the idle timeout
|
|
179
|
+
* cancels it, before its blocked thread resumes it. The channel then drops the operation.
|
|
180
|
+
*/
|
|
181
|
+
@Test
|
|
182
|
+
fun operationDroppedByCancelledReceiveSettlesWhenTransactionEnds() {
|
|
183
|
+
val session = acceptedSession()
|
|
184
|
+
val receiverThread = Executors.newSingleThreadExecutor()
|
|
185
|
+
val receiverBlocked = CountDownLatch(1)
|
|
186
|
+
val releaseReceiver = CountDownLatch(1)
|
|
187
|
+
val deferred = CompletableDeferred<WritableMap>()
|
|
188
|
+
val operation = SynchroModule.TransactionOp.Execute("UPDATE items SET name = 'dropped'", emptyArray(), deferred)
|
|
189
|
+
val timeout = SessionError("idle timeout")
|
|
190
|
+
val delivered = AtomicReference<ChannelResult<SynchroModule.TransactionOp>?>()
|
|
191
|
+
try {
|
|
192
|
+
runBlocking {
|
|
193
|
+
withTimeout(TimeUnit.SECONDS.toMillis(GUARD_SECONDS)) {
|
|
194
|
+
val receive = async(receiverThread.asCoroutineDispatcher()) {
|
|
195
|
+
delivered.set(session.operations.receiveCatching())
|
|
196
|
+
}
|
|
197
|
+
// The receive suspends before this task runs, because the executor has one thread.
|
|
198
|
+
receiverThread.execute {
|
|
199
|
+
receiverBlocked.countDown()
|
|
200
|
+
releaseReceiver.await()
|
|
201
|
+
}
|
|
202
|
+
assertTrue(receiverBlocked.await(GUARD_SECONDS, TimeUnit.SECONDS))
|
|
203
|
+
assertTrue(session.submit(operation))
|
|
204
|
+
// The receive waits, so the rendezvous transfer completes and send returns.
|
|
205
|
+
session.operations.send(operation)
|
|
206
|
+
receive.cancel()
|
|
207
|
+
releaseReceiver.countDown()
|
|
208
|
+
receive.join()
|
|
209
|
+
assertTrue(receive.isCancelled)
|
|
210
|
+
assertEquals("the forced schedule delivered the operation", null, delivered.get())
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
assertFalse(deferred.isCompleted)
|
|
214
|
+
|
|
215
|
+
// The job does this on idle timeout: it aborts in its catch and ends in its finally.
|
|
216
|
+
session.abort(timeout)
|
|
217
|
+
session.end()
|
|
218
|
+
|
|
219
|
+
assertTrue("dropped operation lost its completion owner", deferred.isCompleted)
|
|
220
|
+
assertSame(timeout, deferred.getCompletionExceptionOrNull())
|
|
221
|
+
val late = CompletableDeferred<WritableMap>()
|
|
222
|
+
assertFalse(session.submit(SynchroModule.TransactionOp.Execute("SELECT 1", emptyArray(), late)))
|
|
223
|
+
assertSame(timeout, late.getCompletionExceptionOrNull())
|
|
224
|
+
} finally {
|
|
225
|
+
releaseReceiver.countDown()
|
|
226
|
+
receiverThread.shutdown()
|
|
227
|
+
assertTrue(receiverThread.awaitTermination(GUARD_SECONDS, TimeUnit.SECONDS))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private fun acceptedSession(): SynchroModule.TransactionSession {
|
|
232
|
+
val begin = BeginRecorder()
|
|
233
|
+
val session = SynchroModule.TransactionSession(isWrite = true, beginSettlement = begin::record)
|
|
234
|
+
session.acceptBegin("terminal")
|
|
235
|
+
assertEquals(listOf("terminal"), begin.successes)
|
|
236
|
+
return session
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private class LoopResult(
|
|
240
|
+
val session: SynchroModule.TransactionSession,
|
|
241
|
+
val received: ReceivedTerminal,
|
|
242
|
+
val begin: BeginRecorder,
|
|
243
|
+
val sqlCalls: Int,
|
|
244
|
+
val loopFailure: Throwable?,
|
|
245
|
+
) {
|
|
246
|
+
fun requireOwnedUntilFinish() {
|
|
247
|
+
assertEquals(listOf("wiring"), begin.successes)
|
|
248
|
+
assertEquals(0, sqlCalls)
|
|
249
|
+
assertFalse(received.deferred.isCompleted)
|
|
250
|
+
assertEquals(0, received.deliveryCount)
|
|
251
|
+
val exit = SessionError("selected transaction exit")
|
|
252
|
+
session.finishTerminal(exit)
|
|
253
|
+
assertTrue("loop did not transfer the received terminal to the session", received.deferred.isCompleted)
|
|
254
|
+
assertSame(exit, received.deferred.getCompletionExceptionOrNull())
|
|
255
|
+
assertEquals(1, received.deliveryCount)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Runs the actual loop on an owned thread and sends one terminal operation through its channel. */
|
|
260
|
+
private fun runTerminalLoop(commit: Boolean): LoopResult {
|
|
261
|
+
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
262
|
+
val module = SynchroModule(bridgeTestContext(context))
|
|
263
|
+
val begin = BeginRecorder()
|
|
264
|
+
val session = SynchroModule.TransactionSession(isWrite = true, beginSettlement = begin::record)
|
|
265
|
+
val received = ReceivedTerminal()
|
|
266
|
+
val sqlCalls = AtomicInteger()
|
|
267
|
+
val loopThread = Executors.newSingleThreadExecutor()
|
|
268
|
+
var primary: Throwable? = null
|
|
269
|
+
try {
|
|
270
|
+
val loop = loopThread.submit(Callable {
|
|
271
|
+
module.runTransactionLoop(
|
|
272
|
+
txID = "wiring",
|
|
273
|
+
session = session,
|
|
274
|
+
query = { _, _ -> sqlCalls.incrementAndGet(); emptyList() },
|
|
275
|
+
queryOne = { _, _ -> sqlCalls.incrementAndGet(); null },
|
|
276
|
+
execute = { _, _ -> sqlCalls.incrementAndGet(); ExecResult(rowsAffected = 0) },
|
|
277
|
+
)
|
|
278
|
+
})
|
|
279
|
+
val operation = if (commit) {
|
|
280
|
+
SynchroModule.TransactionOp.Commit(received.deferred)
|
|
281
|
+
} else {
|
|
282
|
+
SynchroModule.TransactionOp.Rollback(received.deferred)
|
|
283
|
+
}
|
|
284
|
+
runBlocking { withTimeout(TimeUnit.SECONDS.toMillis(GUARD_SECONDS)) { session.operations.send(operation) } }
|
|
285
|
+
val failure = try {
|
|
286
|
+
loop.get(GUARD_SECONDS, TimeUnit.SECONDS)
|
|
287
|
+
null
|
|
288
|
+
} catch (error: ExecutionException) {
|
|
289
|
+
error.cause
|
|
290
|
+
}
|
|
291
|
+
return LoopResult(session, received, begin, sqlCalls.get(), failure)
|
|
292
|
+
} catch (error: Throwable) {
|
|
293
|
+
primary = error
|
|
294
|
+
// Closing the actual session channel ends a loop that still waits for an operation.
|
|
295
|
+
session.abort(error)
|
|
296
|
+
throw error
|
|
297
|
+
} finally {
|
|
298
|
+
loopThread.shutdown()
|
|
299
|
+
val cleanup = try {
|
|
300
|
+
if (loopThread.awaitTermination(GUARD_SECONDS, TimeUnit.SECONDS)) {
|
|
301
|
+
null
|
|
302
|
+
} else {
|
|
303
|
+
AssertionError("loop thread did not stop")
|
|
304
|
+
}
|
|
305
|
+
} catch (error: InterruptedException) {
|
|
306
|
+
Thread.currentThread().interrupt()
|
|
307
|
+
error
|
|
308
|
+
}
|
|
309
|
+
if (cleanup != null) {
|
|
310
|
+
primary?.addSuppressed(cleanup) ?: throw cleanup
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|