Skip to content

Commit

Permalink
Add purgeAll API
Browse files Browse the repository at this point in the history
Currently, it is possible to explicitly delete in-memory data for queries through the `perform` function or the mutation
update process.

In #89, we added support for the Subscription API, but there is still no API available to clear in-memory data for subscriptions.
Therefore, we have implemented the `purgeAll` function, which is intended for full reset operations, such as during
sign-out processes.
  • Loading branch information
ogaclejapan committed Sep 29, 2024
1 parent 5bda7b2 commit 09a6b99
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SwrPreviewClient(
override val errorRelay: Flow<ErrorRecord> = flow { }
) : SwrClient, SwrClientPlus, QueryClient by query, MutationClient by mutation, SubscriptionClient by subscription {
override fun gc(level: MemoryPressureLevel) = Unit
override fun purgeAll() = Unit
override fun perform(sideEffects: QueryEffect): Job = Job()
override fun onMount(id: String) = Unit
override fun onUnmount(id: String) = Unit
Expand Down
19 changes: 19 additions & 0 deletions soil-query-core/src/commonMain/kotlin/soil/query/SwrCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ open class SwrCache(private val policy: SwrCachePolicy) : SwrClient, QueryMutabl
}
}

override fun purgeAll() {
purgeAllQueries()
purgeAllMutations()
}

private fun purgeAllQueries() {
val queryStoreCopy = queryStore.toMap()
queryStore.clear()
queryCache.clear()
queryStoreCopy.values.forEach { it.close() }
}

private fun purgeAllMutations() {
val mutationStoreCopy = mutationStore.toMap()
mutationStore.clear()
mutationStoreCopy.values.forEach { it.close() }
}

override fun perform(sideEffects: QueryEffect): Job {
return coroutineScope.launch {
with(this@SwrCache) { sideEffects() }
Expand Down Expand Up @@ -577,6 +595,7 @@ open class SwrCache(private val policy: SwrCachePolicy) : SwrClient, QueryMutabl
return model?.let(predicate) ?: false
}


internal class ManagedMutation<T>(
val scope: CoroutineScope,
val id: UniqueId,
Expand Down
15 changes: 15 additions & 0 deletions soil-query-core/src/commonMain/kotlin/soil/query/SwrCachePlus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class SwrCachePlus(private val policy: SwrCachePlusPolicy) : SwrCache(policy), S
)
private val batchScheduler: BatchScheduler = policy.batchSchedulerFactory.create(coroutineScope)


// ----- SwrClientPlus ----- //

override val defaultSubscriptionOptions: SubscriptionOptions = policy.subscriptionOptions

override fun gc(level: MemoryPressureLevel) {
Expand All @@ -58,6 +61,18 @@ class SwrCachePlus(private val policy: SwrCachePlusPolicy) : SwrCache(policy), S
}
}

override fun purgeAll() {
super.purgeAll()
purgeAllSubscriptions()
}

private fun purgeAllSubscriptions() {
val subscriptionStoreCopy = subscriptionStore.toMap()
subscriptionStore.clear()
subscriptionCache.clear()
subscriptionStoreCopy.values.forEach { it.close() }
}

@Suppress("UNCHECKED_CAST")
override fun <T> getSubscription(
key: SubscriptionKey<T>,
Expand Down
9 changes: 9 additions & 0 deletions soil-query-core/src/commonMain/kotlin/soil/query/SwrClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ interface SwrClient : MutationClient, QueryClient {
*/
fun gc(level: MemoryPressureLevel = MemoryPressureLevel.Low)

/**
* Removes all queries and mutations from the in-memory stored data.
*
* **Note:**
* If there are any active queries or mutations, they will be stopped as well.
* This method should only be used for full resets, such as during sign-out.
*/
fun purgeAll()

/**
* Executes side effects for queries.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@ package soil.query
/**
* An enhanced version of [SwrClient] that integrates [SubscriptionClient] into SwrClient.
*/
interface SwrClientPlus : SwrClient, SubscriptionClient
interface SwrClientPlus : SwrClient, SubscriptionClient {

/**
* Removes all queries, mutations and subscriptions from the in-memory stored data.
*
* **Note:**
* If there are any active queries or mutations or subscriptions, they will be stopped as well.
* This method should only be used for full resets, such as during sign-out.
*/
override fun purgeAll()
}
20 changes: 11 additions & 9 deletions soil-query-core/src/commonMain/kotlin/soil/query/core/Actor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,39 @@ interface Actor {
fun launchIn(scope: CoroutineScope): Job
}

internal typealias ActorSequenceNumber = Int
internal typealias ActorSequenceNumber = String

internal class ActorBlockRunner(
private val id: String = uuid(),
private val scope: CoroutineScope,
private val options: ActorOptions,
private val onTimeout: (ActorSequenceNumber) -> Unit,
private val block: suspend () -> Unit
) : Actor {

var seq: ActorSequenceNumber = 0
private set
val seq: ActorSequenceNumber
get() = "$id#$versionCounter"

private var launchedCount: Int = 0
private var versionCounter: Int = 0
private var activeCounter: Int = 0
private var hasActiveScope: Boolean = false
private var runningJob: Job? = null
private var cancellationJob: Job? = null

override fun launchIn(scope: CoroutineScope): Job {
seq++
versionCounter++
return scope.launch(start = CoroutineStart.UNDISPATCHED) {
cancellationJob?.cancelAndJoin()
cancellationJob = null
suspendCancellableCoroutine { continuation ->
launchedCount++
if (!hasActiveScope && launchedCount > 0) {
activeCounter++
if (!hasActiveScope && activeCounter > 0) {
hasActiveScope = true
start()
}
continuation.invokeOnCancellation {
launchedCount--
if (hasActiveScope && launchedCount <= 0) {
activeCounter--
if (hasActiveScope && activeCounter <= 0) {
hasActiveScope = false
stop()
}
Expand Down
58 changes: 31 additions & 27 deletions soil-query-core/src/commonTest/kotlin/soil/query/core/ActorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.yield
import soil.testing.UnitTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

Expand All @@ -29,31 +30,32 @@ class ActorTest : UnitTest() {
val blockHandler = TestBlockHandler()
val timeoutHandler = TestTimeoutHandler()
val actor = ActorBlockRunner(
id = "test",
scope = backgroundScope,
options = TestActorOptions(),
onTimeout = timeoutHandler::onTimeout,
block = blockHandler::handle
)

assertEquals(0, actor.seq)
assertEquals("test#0", actor.seq)
assertEquals(0, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

val scope1 = CoroutineScope(uiDispatcher)
val job1 = actor.launchIn(scope1)
yield()

assertEquals(1, actor.seq)
assertEquals("test#1", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

job1.cancel()
// Wait for the actor to be canceled. (keepAliveTime = 5.seconds)
advanceTimeBy(6.seconds)

assertEquals(1, actor.seq)
assertEquals("test#1", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(1, timeoutHandler.seq)
assertEquals("test#1", timeoutHandler.seq)
}

@Test
Expand All @@ -63,46 +65,47 @@ class ActorTest : UnitTest() {
val blockHandler = TestBlockHandler()
val timeoutHandler = TestTimeoutHandler()
val actor = ActorBlockRunner(
id = "test",
scope = backgroundScope,
options = TestActorOptions(),
onTimeout = timeoutHandler::onTimeout,
block = blockHandler::handle
)

assertEquals(0, actor.seq)
assertEquals("test#0", actor.seq)
assertEquals(0, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

val scope1 = CoroutineScope(uiDispatcher)
val job1 = actor.launchIn(scope1)
yield()

assertEquals(1, actor.seq)
assertEquals("test#1", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

val scope2 = CoroutineScope(uiDispatcher)
val job2 = actor.launchIn(scope2)
yield()

assertEquals(2, actor.seq)
assertEquals("test#2", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

job1.cancel()
yield()

assertEquals(2, actor.seq)
assertEquals("test#2", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

job2.cancel()

// Wait for the actor to be canceled. (keepAliveTime = 5.seconds)
advanceTimeBy(6.seconds)
assertEquals(2, actor.seq)
assertEquals("test#2", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(2, timeoutHandler.seq)
assertEquals("test#2", timeoutHandler.seq)
}

@Test
Expand All @@ -113,31 +116,32 @@ class ActorTest : UnitTest() {
val blockHandler = TestBlockHandler()
val timeoutHandler = TestTimeoutHandler()
val actor = ActorBlockRunner(
id = "test",
scope = actorScope,
options = TestActorOptions(),
onTimeout = timeoutHandler::onTimeout,
block = blockHandler::handle
)

assertEquals(0, actor.seq)
assertEquals("test#0", actor.seq)
assertEquals(0, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

val scope1 = CoroutineScope(uiDispatcher)
val job1 = actor.launchIn(scope1)
yield()

assertEquals(1, actor.seq)
assertEquals("test#1", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(-1, timeoutHandler.seq)
assertNull(timeoutHandler.seq)

job1.cancel()

// Wait for the actor to be canceled. (keepAliveTime = 5.seconds)
advanceTimeBy(6.seconds)
assertEquals(1, actor.seq)
assertEquals("test#1", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(1, timeoutHandler.seq)
assertEquals("test#1", timeoutHandler.seq)

actorScope.cancel()
advanceUntilIdle()
Expand All @@ -146,17 +150,17 @@ class ActorTest : UnitTest() {
val job2 = actor.launchIn(scope2)
yield()

assertEquals(2, actor.seq)
assertEquals("test#2", actor.seq)
// Unchanged (already canceled)
assertEquals(1, blockHandler.count)
assertEquals(1, timeoutHandler.seq)
assertEquals("test#1", timeoutHandler.seq)

job2.cancel()

advanceTimeBy(6.seconds)
assertEquals(2, actor.seq)
assertEquals("test#2", actor.seq)
assertEquals(1, blockHandler.count)
assertEquals(1, timeoutHandler.seq)
assertEquals("test#1", timeoutHandler.seq)
}

private class TestBlockHandler {
Expand All @@ -173,7 +177,7 @@ class ActorTest : UnitTest() {
}

private class TestTimeoutHandler {
var seq: ActorSequenceNumber = -1
var seq: ActorSequenceNumber? = null
private set

fun onTimeout(seq: ActorSequenceNumber) {
Expand Down

0 comments on commit 09a6b99

Please sign in to comment.