-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Created data-shared module and KeyValueStorage components
- Loading branch information
Showing
7 changed files
with
619 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,4 +65,8 @@ public class ServiceTokensSource @Inject public constructor( | |
} | ||
} | ||
} | ||
|
||
override suspend fun clear() { | ||
// TODO | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||
|
||
plugins { | ||
kotlin("plugin.serialization") | ||
id("com.android.library") | ||
id("org.jetbrains.compose") | ||
id("org.jetbrains.kotlin.plugin.compose") | ||
id("org.jetbrains.dokka") | ||
id("mooncloak.multiplatform") | ||
} | ||
|
||
kotlin { | ||
sourceSets { | ||
all { | ||
// Disable warnings and errors related to these expected @OptIn annotations. | ||
// See: https://kotlinlang.org/docs/opt-in-requirements.html#module-wide-opt-in | ||
languageSettings.optIn("kotlin.RequiresOptIn") | ||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") | ||
languageSettings.optIn("kotlinx.coroutines.FlowPreview") | ||
languageSettings.optIn("kotlin.time.ExperimentalTime") | ||
languageSettings.optIn("com.chrynan.navigation.ExperimentalNavigationApi") | ||
languageSettings.optIn("-Xexpect-actual-classes") | ||
} | ||
|
||
val commonMain by getting { | ||
dependencies { | ||
// Coroutines | ||
// https://github.com/Kotlin/kotlinx.coroutines | ||
implementation(KotlinX.coroutines.core) | ||
|
||
// Serialization | ||
// https://github.com/Kotlin/kotlinx.serialization | ||
implementation(KotlinX.serialization.json) | ||
|
||
// Time | ||
// https://github.com/Kotlin/kotlinx-datetime | ||
implementation(KotlinX.datetime) | ||
|
||
implementation(compose.runtime) | ||
|
||
// Multiplatform Key/Value Storage | ||
// https://github.com/russhwolf/multiplatform-settings | ||
// Apache 2.0: https://github.com/russhwolf/multiplatform-settings/blob/main/LICENSE.txt | ||
api(RussHWolf.multiplatformSettings.settings) | ||
api(RussHWolf.multiplatformSettings.noArg) | ||
implementation(RussHWolf.multiplatformSettings.coroutines) | ||
implementation(RussHWolf.multiplatformSettings.serialization) | ||
} | ||
} | ||
|
||
val commonTest by getting { | ||
dependencies { | ||
implementation(kotlin("test")) | ||
implementation(KotlinX.coroutines.test) | ||
} | ||
} | ||
|
||
val jsMain by getting { | ||
dependencies { | ||
} | ||
} | ||
|
||
val wasmJsMain by getting { | ||
dependencies { | ||
} | ||
} | ||
} | ||
} | ||
|
||
android { | ||
compileSdk = AppConstants.Android.compileSdkVersion | ||
namespace = "com.mooncloak.vpn.data.shared" | ||
|
||
defaultConfig { | ||
minSdk = AppConstants.Android.minSdkVersion | ||
targetSdk = AppConstants.Android.targetSdkVersion | ||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
|
||
buildTypes { | ||
getByName("release") { | ||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") | ||
} | ||
} | ||
|
||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
|
||
tasks.withType<KotlinCompile> { | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
// Opt-in to experimental compose APIs | ||
freeCompilerArgs = listOf( | ||
"-Xopt-in=kotlin.RequiresOptIn" | ||
) | ||
} | ||
} | ||
} |
149 changes: 149 additions & 0 deletions
149
data-shared/src/commonMain/kotlin/com/mooncloak/vpn/data/shared/InMemoryKeyValueStorage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package com.mooncloak.vpn.data.shared | ||
|
||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.sync.Mutex | ||
import kotlinx.coroutines.sync.withLock | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.StringFormat | ||
|
||
/** | ||
* An in-memory implementation of [KeyValueStorage], [MutableKeyValueStorage], and [FlowableKeyValueStorage]. | ||
* | ||
* This class provides a simple, in-memory key-value storage solution suitable for testing or applications where data | ||
* persistence across sessions is not required. It stores data as strings internally and uses a provided [StringFormat] | ||
* for serialization and deserialization of values. | ||
* | ||
* This implementation is thread-safe, allowing concurrent access and modifications from multiple coroutines. It also | ||
* supports observing changes to specific keys via Kotlin Flows. | ||
* | ||
* @property [format] The [StringFormat] used for serializing and deserializing values to and from strings. Typically, | ||
* this will be a JSON format, but other string-based formats are supported. | ||
*/ | ||
public class InMemoryKeyValueStorage public constructor( | ||
private val format: StringFormat | ||
) : KeyValueStorage, | ||
MutableKeyValueStorage, | ||
FlowableKeyValueStorage { | ||
|
||
private val storage = mutableMapOf<String, String>() | ||
|
||
private val listeners = mutableMapOf<String, MutableStateFlow<String?>>() | ||
|
||
private val mutex = Mutex(locked = false) | ||
private val emitMutex = Mutex(locked = false) | ||
|
||
override suspend fun contains(key: String): Boolean { | ||
require(key.isNotBlank()) { "Key must not be blank." } | ||
|
||
return storage.contains(key) | ||
} | ||
|
||
override suspend fun <Value : Any> get(key: String, deserializer: KSerializer<Value>): Value? { | ||
require(key.isNotBlank()) { "Key must not be blank." } | ||
|
||
val stored = storage[key] ?: return null | ||
|
||
return format.decodeFromString( | ||
deserializer = deserializer, | ||
string = stored | ||
) | ||
} | ||
|
||
override suspend fun <Value : Any> set(key: String, value: Value?, serializer: KSerializer<Value>) { | ||
require(key.isNotBlank()) { "Key must not be blank." } | ||
|
||
mutex.withLock { | ||
if (value == null) { | ||
storage.remove(key) | ||
|
||
emit(key = key, value = null) | ||
} else { | ||
val stored = format.encodeToString( | ||
serializer = serializer, | ||
value = value | ||
) | ||
|
||
storage[key] = stored | ||
|
||
emit(key = key, value = stored) | ||
} | ||
} | ||
} | ||
|
||
override suspend fun remove(key: String) { | ||
require(key.isNotBlank()) { "Key must not be blank." } | ||
|
||
mutex.withLock { | ||
storage.remove(key) | ||
|
||
emit(key = key, value = null) | ||
} | ||
} | ||
|
||
override suspend fun clear() { | ||
mutex.withLock { | ||
val currentKeys = storage.keys | ||
|
||
storage.clear() | ||
|
||
currentKeys.forEach { key -> | ||
emit(key = key, value = null) | ||
} | ||
} | ||
} | ||
|
||
override fun <Value : Any> flow(key: String, deserializer: KSerializer<Value>): Flow<Value?> { | ||
require(key.isNotBlank()) { "Key must not be blank." } | ||
|
||
val flow = listeners[key] ?: MutableStateFlow<String?>(null).apply { | ||
value = storage[key] | ||
} | ||
|
||
listeners[key] = flow | ||
|
||
return flow.map { stored -> | ||
if (stored != null) { | ||
format.decodeFromString( | ||
deserializer = deserializer, | ||
string = stored | ||
) | ||
} else { | ||
null | ||
} | ||
} | ||
} | ||
|
||
private suspend fun emit(key: String, value: String?) { | ||
emitMutex.withLock { | ||
val listener = listeners[key] | ||
|
||
listener?.value = value | ||
} | ||
} | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is InMemoryKeyValueStorage) return false | ||
|
||
if (format != other.format) return false | ||
if (storage != other.storage) return false | ||
if (listeners != other.listeners) return false | ||
if (mutex != other.mutex) return false | ||
|
||
return emitMutex == other.emitMutex | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = format.hashCode() | ||
result = 31 * result + storage.hashCode() | ||
result = 31 * result + listeners.hashCode() | ||
result = 31 * result + mutex.hashCode() | ||
result = 31 * result + emitMutex.hashCode() | ||
return result | ||
} | ||
|
||
override fun toString(): String = | ||
"InMemoryKeyValueStorage(format=$format, storage=$storage, listeners=$listeners, mutex=$mutex, emitMutex=$emitMutex)" | ||
} |
Oops, something went wrong.