Skip to content

Commit

Permalink
Created data-shared module and KeyValueStorage components
Browse files Browse the repository at this point in the history
  • Loading branch information
chRyNaN committed Feb 25, 2025
1 parent b893a5f commit 055c9e9
Show file tree
Hide file tree
Showing 7 changed files with 619 additions and 0 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ public class ServiceTokensSource @Inject public constructor(
}
}
}

override suspend fun clear() {
// TODO
}
}
100 changes: 100 additions & 0 deletions data-shared/build.gradle.kts
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"
)
}
}
}
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)"
}
Loading

0 comments on commit 055c9e9

Please sign in to comment.