diff --git a/README.md b/README.md index 32aacdb..f60f12b 100644 --- a/README.md +++ b/README.md @@ -13,49 +13,23 @@ We really want someone to use our library, ask us about any questions and give s ### Storage ```kotlin -object Storage : KDataStorage() { // or KDataStorage("name") or KDataStorage({ path("...") }) - var launchesCount by property(0) - var list by property(mutableListOf()) -} +val storage = KFileDataStorage(name = "data.json") +// Or +val storage = KFileDataStorage(absolutePath = "...") + +var launchesCount by storage.property { 0 } +var list by storage.property { mutableListOf() } -suspend fun main() = Storage.commitMutate { +fun main() = storage.mutateBlocking { list += "Element" launchesCount++ } ``` -### Value Storage -```kotlin -val storage = KValueStorage(0) -val launchesCount by storage - -suspend fun main() = storage.commitMutate { - println("${++launchesCount}") -} -``` +There are both blocking and asynchronous implementations (except JS-browser where there is only blocking implementation due to using `localStorage` instead of files). -## Non-coroutines way -There is also a way for changing the storage without suspend context in infinitely-running apps: -```kotlin -class KindaActivity : CoroutineScope by ... { - // Scope is still optional - object Storage : KDataStorage(scope = this) { // or KDataStorage("name") or KDataStorage({ path("...") }) - var launchesCount by property(0) - var list by property(mutableListOf()) - } - - fun onCreate() = with(Storage) { - // Commit launches automatically since there is delegate call - launchesCount++ - - // However, list is a mutable object, so explicit mutation declaration required - mutate { - list += "Element" - } - } -} -``` +Library may be fully customized since you can implement your own [DataManager](src/commonMain/kotlin/fun/kotlingang/kds/manager/DataManager.kt) ## Installation `$version` - library version, can be found in badge above @@ -68,7 +42,7 @@ repositories { } } dependencies { - implementation "fun.kotlingang.kds:kds:$version" + implementation "fun.kotlingang.kds:kds-{platformSuffix}:$version" } ``` ### Kotlin Gradle Dsl @@ -77,7 +51,7 @@ repositories { maven("https://maven.kotlingang.fun/") } dependencies { - implementation("fun.kotlingang.kds:kds:$version") + implementation("fun.kotlingang.kds:kds-{platformSuffix}:$version") } ``` -> For nodejs use `fun.kotlingang.kds:kds-node:$version` dependency +> `platformSuffix` (js, jvm, etc) is suffix required when you using the library not from common code, for nodejs use `node` suffix. diff --git a/build.gradle.kts b/build.gradle.kts index ce38786..ff63ec9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,30 +32,41 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(utils) implementation(coroutines) api(serialization) } } + val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } - val nodeMain by getting { - dependencies { - implementation(nodejsExternals) - } + + val filesTarget by creating { + dependsOn(commonMain) + } + + val jvmMain by getting { + dependsOn(filesTarget) } val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) } } - val jsTest by getting { + + val commonJsMain by creating { + dependsOn(commonMain) + } + + val nodeMain by getting { + dependsOn(filesTarget) + dependsOn(commonJsMain) + dependencies { - implementation(kotlin("test-js")) + implementation(nodejsExternals) } } val nodeTest by getting { @@ -64,6 +75,15 @@ kotlin { } } + val jsMain by getting { + dependsOn(commonJsMain) + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + all { languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") } diff --git a/buildSrc/src/main/kotlin/AppInfo.kt b/buildSrc/src/main/kotlin/AppInfo.kt index 3ea12ef..5ba5208 100644 --- a/buildSrc/src/main/kotlin/AppInfo.kt +++ b/buildSrc/src/main/kotlin/AppInfo.kt @@ -1,7 +1,6 @@ object AppInfo { const val PACKAGE = "fun.kotlingang.kds" - const val VERSION = "2.6" - const val ARTIFACT_ID = "kds" + const val VERSION = "3.0.3" const val NAME = "Kotlin Data Storage" const val DESCRIPTION = "Multiplatform Coroutine-Based Kotlin Library for storing data via kotlinx.serialization" } diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt index d24d806..8b13789 100644 --- a/buildSrc/src/main/kotlin/Modules.kt +++ b/buildSrc/src/main/kotlin/Modules.kt @@ -1,4 +1 @@ -import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler - -val KotlinDependencyHandler.utils get() = project(":utils") diff --git a/settings.gradle.kts b/settings.gradle.kts index e7a5f27..dd3df72 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1 @@ rootProject.name = "kds" - -include("utils") diff --git a/src/commonJsMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt b/src/commonJsMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt new file mode 100644 index 0000000..2e29a4a --- /dev/null +++ b/src/commonJsMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt @@ -0,0 +1,4 @@ +package `fun`.kotlingang.kds.sync + + +internal actual inline fun platformSynchronized(lock: Any, block: () -> R) = block() diff --git a/src/commonJsMain/kotlin/fun/kotlingang/kds/test/launchTest.kt b/src/commonJsMain/kotlin/fun/kotlingang/kds/test/launchTest.kt new file mode 100644 index 0000000..32efb5b --- /dev/null +++ b/src/commonJsMain/kotlin/fun/kotlingang/kds/test/launchTest.kt @@ -0,0 +1,8 @@ +package `fun`.kotlingang.kds.test + +import `fun`.kotlingang.kds.extensions.any.unit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + + +actual fun CoroutineScope.launchTest(block: suspend CoroutineScope.() -> Unit) = launch { block() }.unit diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/KAsyncDataStorage.kt b/src/commonMain/kotlin/fun/kotlingang/kds/KAsyncDataStorage.kt new file mode 100644 index 0000000..b01b7d3 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/KAsyncDataStorage.kt @@ -0,0 +1,54 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package `fun`.kotlingang.kds + +import `fun`.kotlingang.kds.composition.AsyncCommitPerformer +import `fun`.kotlingang.kds.composition.AsyncCommittable +import `fun`.kotlingang.kds.manager.AsyncDataManager +import `fun`.kotlingang.kds.sync.platformSynchronized +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + + +class KAsyncDataStorage @OptIn(DelicateCoroutinesApi::class) constructor ( + json: Json = Json, + scope: CoroutineScope = GlobalScope + SupervisorJob() + CoroutineName("KDS Coroutine"), + private val manager: AsyncDataManager +) : KBlockingDataStorage(json, manager) { + private var asyncData: Map? = null + + override fun getOrLoadData() = platformSynchronized(lock = this) { + asyncData ?: manager.loadDataBlocking().parseData().also { asyncData = it } + }.toMutableMap() + + private val loadDataMutex = Mutex() + + /** + * The worst case may be when data will be loaded twice but it is not problem + * since library is recommended to use on small data. + * *It is impossible when using functions with their contacts. + */ + suspend fun loadData() = loadDataMutex.withLock { + if(asyncData != null) { + val data = manager.loadData().parseData() + platformSynchronized(lock = this) { + if (asyncData != null) { + asyncData = data + } + } + } + } + + private val commitPerformer = AsyncCommitPerformer(scope) { manager.saveData(data.encodeData()) } + + suspend fun commit() = commitPerformer.commit() + fun launchCommit() = commitPerformer.launchCommit() + + override fun performAutoSave() { + if(autoSave) + launchCommit() + } +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/KBlockingDataStorage.kt b/src/commonMain/kotlin/fun/kotlingang/kds/KBlockingDataStorage.kt new file mode 100644 index 0000000..d5799c6 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/KBlockingDataStorage.kt @@ -0,0 +1,94 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package `fun`.kotlingang.kds + +import `fun`.kotlingang.kds.annotation.DelicateKDSApi +import `fun`.kotlingang.kds.composition.AutoSaveController +import `fun`.kotlingang.kds.composition.JsonReferencesProxy +import `fun`.kotlingang.kds.extensions.any.unit +import `fun`.kotlingang.kds.manager.BlockingDataManager +import `fun`.kotlingang.kds.mutate.* +import kotlinx.serialization.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + + +open class KBlockingDataStorage ( + val json: Json = Json, + private val manager: BlockingDataManager +) { + @DelicateKDSApi + val autoSaveController = AutoSaveController() + @OptIn(DelicateKDSApi::class) + val autoSave by autoSaveController::autoSave + + /** + * [mutate], [mutateBlocking], [mutateCommit] should be used instead + */ + @DelicateKDSApi + inline fun withoutSave(crossinline block: () -> Unit) { + autoSaveController.turnOff() + block() + autoSaveController.tryTurnOn() + } + + protected open fun getOrLoadData() = manager.loadDataBlocking().parseData().toMutableMap() + + private val dataProxy by lazy { + JsonReferencesProxy(json, getOrLoadData()) + } + + protected val data get() = dataProxy.publicData + + fun loadDataBlocking() = dataProxy.unit + + /** + * Stores all references to data in case references were changed + */ + @DelicateKDSApi + fun applyMutations() = dataProxy.applyMutations() + + @DelicateKDSApi + fun set(name: String, serializer: KSerializer, value: T) { + dataProxy.set(name, serializer, value) + performAutoSave() + } + + @DelicateKDSApi + inline fun set(name: String, value: @Serializable T) = + set(name, json.serializersModule.serializer(), value) + + @DelicateKDSApi + fun get(name: String, serializer: KSerializer, default: () -> T) = + dataProxy.get(name, serializer, default) + + @DelicateKDSApi + inline fun get(name: String, noinline default: () -> T) = + get(name, json.serializersModule.serializer(), default) + + fun exists(name: String) = dataProxy.exists(name) + + fun clear(name: String) { + dataProxy.clear(name) + performAutoSave() + } + + fun clear() { + dataProxy.clear() + performAutoSave() + } + + fun commitBlocking() = manager.saveDataBlocking(data.encodeData()) + + /** + * For [KBlockingDataStorage] auto save way is blocking way while + * for [KAsyncDataStorage] auto save way is async way + */ + protected open fun performAutoSave() { + if(autoSave) + commitBlocking() + } + + protected fun String.parseData() = json.decodeFromString>(string = this) + protected fun Map.encodeData() = json.encodeToString(value = this) +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/KDataStorage.kt b/src/commonMain/kotlin/fun/kotlingang/kds/KDataStorage.kt deleted file mode 100644 index 55f1542..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/KDataStorage.kt +++ /dev/null @@ -1,172 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate") - -package `fun`.kotlingang.kds - -import `fun`.kotlingang.kds.builder.StorageConfig -import `fun`.kotlingang.kds.delegate.KDataStorageProperty -import `fun`.kotlingang.kds.storage.BaseStorage -import `fun`.kotlingang.kds.storage.dirPath -import `fun`.kotlingang.kds.storage.joinPath -import kotlinx.coroutines.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.serializer -import kotlin.coroutines.CoroutineContext -import kotlin.reflect.KClass - - -typealias StorageConfigBuilder = StorageConfig.() -> Unit - -/** - * If you are using mutable objects, do not forget to use [KDataStorage.mutate] - */ -open class KDataStorage ( - /** - * Scope there used only for cases when program finishes nut there are still some jobs launched - */ - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope + SupervisorJob(), - builder: StorageConfigBuilder = {} -) { - constructor ( - name: String, - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope, - builder: StorageConfigBuilder = {} - ) : this ( - coroutineScope, { name(name); builder() } - ) - - private val scope = coroutineScope + Job() + CoroutineName(name = "KDS Coroutine") - - internal val blockingLocker = PlatformLocker() - - private val defaultPath = dirPath.joinPath("data") - private val config = StorageConfig(defaultPath).apply(builder) - - private val path = config.path ?: defaultPath.joinPath(this::class.getDefaultFilename() + ".json") - val json = config.json - - /* Internal Storage API */ - private val baseStorage = BaseStorage(path) - - // Store for value references. Used to save mutable objects - private val referencesSource: MutableMap>> = mutableMapOf() - internal fun saveReference(name: String, value: T, serializer: KSerializer) { - referencesSource[name] = value to serializer - } - internal fun getReference(name: String): Pair { - val reference = referencesSource[name] - return (reference != null) to reference?.component1() - } - - // Store for encoded values - // Mutable value required in Javascript, so there is no way to run await blocking - private var dataSource: MutableMap? = null - - private val dataLoadingJob = scope.launch { - dataSource = json.decodeFromString(string = baseStorage.loadStorage() ?: "{}") - } - - // When get, try to await it blocking or error - internal val data get() = dataSource ?: runBlockingPlatform { dataLoadingJob.join() }.let { (setup, _) -> - dataSource ?: if(setup) { - error("Internal error, because storage is not loaded. " + - "You shouldn't see this error, please create an issue. " + - "To fix it try awaitLoading before using storage") - } else error("Your target (js) cannot setup storage blocking. Please call awaitLoading() first") - } - - private var savingJob: Job? = null - /** - * Prevents redundant operations when it called one by one. - * [blockingLocker] used there because there is no heavy operations, just thread-safety - */ - private fun privateLaunchCommit() = scope.launch { - blockingLocker.withLock { - savingJob?.cancel() - return@withLock launch { - saveReferencesToData() - // to prevent concurrent modification exception - val json = blockingLocker.withLock { json.encodeToString(data) } - baseStorage.saveStorage(json) - }.also { job -> - savingJob = job - } - }.join() - } - - /** - * Encodes all values from [referencesSource] to JsonElement and puts it to [data]. - * So state of mutable objects will be saved - */ - private fun saveReferencesToData() { - blockingLocker.withLock { - for((name, pair) in referencesSource) { - val (value, serializer) = pair - @Suppress("UNCHECKED_CAST") - fun uncheckedSet(serializer: KSerializer) { - data[name] = json.encodeToJsonElement(serializer, value as T) - } - uncheckedSet(serializer) - } - } - } - - /* ----- */ - - fun property(serializer: KSerializer, lazyDefault: () -> T) = KDataStorageProperty(serializer, lazyDefault) - inline fun property(noinline lazyDefault: () -> T) = property(json.serializersModule.serializer(), lazyDefault) - - fun property(serializer: KSerializer, default: T) = property(serializer) { default } - inline fun property(default: T) = property(json.serializersModule.serializer(), default) - - fun property(serializer: KSerializer) = property(serializer, default = null) - inline fun property() = property(json.serializersModule.serializer()) - - /** - * Await first loading; Should be done before fun.kotlingang.kds.storage usage - */ - suspend fun awaitLoading() = dataLoadingJob.join() - - /** - * Call it if mutable data was changed to commit data async - */ - fun launchCommit() = privateLaunchCommit() - - /** - * Call it if mutable data was changed to commit data sync - */ - suspend fun commit() = privateLaunchCommit().join() - - /** - * Clear property value - */ - fun clear(propertyName: String) { - blockingLocker.withLock { - referencesSource.remove(propertyName) - data.remove(propertyName) - } - launchCommit() - } -} - - -/** - * Edit mutable values inside block - */ -inline fun T.mutate(block: T.() -> Unit) { - block() - launchCommit() -} - -/** - * Like [mutate] but commit is awaited - */ -suspend inline fun T.commitMutate(block: T.() -> Unit) { - awaitLoading() - block() - commit() -} - -private fun KClass<*>.getDefaultFilename() = simpleName ?: "noname" diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/annotation/DelicateKDSApi.kt b/src/commonMain/kotlin/fun/kotlingang/kds/annotation/DelicateKDSApi.kt new file mode 100644 index 0000000..085b263 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/annotation/DelicateKDSApi.kt @@ -0,0 +1,8 @@ +package `fun`.kotlingang.kds.annotation + + +@RequiresOptIn(message = """ +APIs marked with this annotation are recommended to use only if you are *sure* what are you doing and know a function contract. +In most cases it shouldn't be used by end users +""") +annotation class DelicateKDSApi diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/builder/StorageConfig.kt b/src/commonMain/kotlin/fun/kotlingang/kds/builder/StorageConfig.kt deleted file mode 100644 index c20a821..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/builder/StorageConfig.kt +++ /dev/null @@ -1,19 +0,0 @@ -package `fun`.kotlingang.kds.builder - -import `fun`.kotlingang.kds.storage.BaseStorage -import `fun`.kotlingang.kds.storage.joinPath -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonBuilder - -class StorageConfig(private val defaultPath: String) { - internal var path: String? = null - - fun path(absolutePath: String) { path = absolutePath } - fun name(name: String) = path(defaultPath.joinPath("$name.json")) - - - internal var json: Json = Json - - fun json(json: Json) { this.json = json } - fun buildJson(builder: JsonBuilder.() -> Unit) = json(Json { builder() }) -} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/composition/AsyncCommitPerformer.kt b/src/commonMain/kotlin/fun/kotlingang/kds/composition/AsyncCommitPerformer.kt new file mode 100644 index 0000000..a95afec --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/composition/AsyncCommitPerformer.kt @@ -0,0 +1,33 @@ +package `fun`.kotlingang.kds.composition + +import `fun`.kotlingang.kds.extensions.any.unit +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + + +/** + * Used to remove redundant async savings + */ +class AsyncCommitPerformer internal constructor ( + private val scope: CoroutineScope, + private val asyncCommittable: AsyncCommittable +){ + private val mutex = Mutex() + private var job: Job? = null + + suspend fun commit() = mutex.withLock { + job?.cancelAndJoin() + job = scope.launch { + asyncCommittable.asyncCommit() + } + job?.join() ?: error("Contract error") + } + + fun launchCommit() = scope.launch { commit() }.unit +} + +internal fun interface AsyncCommittable { + suspend fun asyncCommit() +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/composition/AutoSaveController.kt b/src/commonMain/kotlin/fun/kotlingang/kds/composition/AutoSaveController.kt new file mode 100644 index 0000000..8ede225 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/composition/AutoSaveController.kt @@ -0,0 +1,17 @@ +package `fun`.kotlingang.kds.composition + +import `fun`.kotlingang.kds.sync.platformSynchronized + + +/** + * Used to achieve thread safety for [autoSave] + */ +class AutoSaveController internal constructor() { + private var turnOffRequests = 0 + + val autoSave get() = turnOffRequests == 0 + + fun turnOff() = platformSynchronized(lock = this) { turnOffRequests++ } + fun turnOn() { turnOffRequests = 0 } + fun tryTurnOn() = platformSynchronized(lock = this) { turnOffRequests-- } +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/composition/JsonReferencesProxy.kt b/src/commonMain/kotlin/fun/kotlingang/kds/composition/JsonReferencesProxy.kt new file mode 100644 index 0000000..bc7db7b --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/composition/JsonReferencesProxy.kt @@ -0,0 +1,73 @@ +package `fun`.kotlingang.kds.composition + +import `fun`.kotlingang.kds.sync.platformSynchronized +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializer + + +/** + * Entity used to store map of [String] to [JsonElement], but + * with additional references storage for handling mutations + */ +internal class JsonReferencesProxy ( + private val json: Json, + data: Map +) { + private val data = data.toMutableMap() + val publicData get() = platformSynchronized(lock = this) { data.toMap() } + + private val references = mutableMapOf, *>>() + + fun get ( + key: String, + serializer: KSerializer, + default: () -> T + ) = platformSynchronized(lock = this) { + if(key in references) + @Suppress("UNCHECKED_CAST") + return@platformSynchronized references[key]!!.second as T + + val element = data[key] ?: return@platformSynchronized default().also { + references[key] = serializer to it + data[key] = json.encodeToJsonElement(serializer, it) + } + + return@platformSynchronized json.decodeFromJsonElement(serializer, element).also { + references[key] = serializer to it + } + } + + fun set ( + key: String, + serializer: KSerializer, + value: T + ) = platformSynchronized(lock = this) { + references[key] = serializer to value + data[key] = json.encodeToJsonElement(serializer, value) + } + + fun exists(key: String) = key in data + + fun clear() = platformSynchronized(lock = this) { + references.clear() + data.clear() + } + + fun clear(key: String) = platformSynchronized(lock = this) { + references.remove(key) + data.remove(key) + } + + fun applyMutations() = platformSynchronized(lock = this) { + @Suppress("UNCHECKED_CAST") + fun encodeUnsafe(serializer: KSerializer, value: Any?) = + json.encodeToJsonElement(serializer, value as T) + + for((k, v) in references) { + val (serializer, value) = v + data[k] = encodeUnsafe(serializer, value) + } + } +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSDelegate.kt b/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSDelegate.kt new file mode 100644 index 0000000..10dd057 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSDelegate.kt @@ -0,0 +1,46 @@ +package `fun`.kotlingang.kds.delegate + +import `fun`.kotlingang.kds.KBlockingDataStorage +import `fun`.kotlingang.kds.annotation.DelicateKDSApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer +import kotlin.reflect.KProperty +import kotlin.reflect.KType +import kotlin.reflect.typeOf + + +@OptIn(ExperimentalStdlibApi::class) +inline fun KDSDelegate ( + noinline default: () -> T +) = KDSDelegate(typeOf(), default) + + +@Suppress("UNCHECKED_CAST") +@OptIn(DelicateKDSApi::class) +class KDSDelegate private constructor ( + /** @contract Union */ private val serializer: KSerializer?, + /** @contract Union */ private val type: KType?, + private val default: () -> T +) { + constructor(serializer: KSerializer?, default: () -> T) : this(serializer, type = null, default) + @PublishedApi + internal constructor(type: KType, default: () -> T) : this(serializer = null, type, default) + + operator fun getValue(storage: KBlockingDataStorage, property: KProperty<*>) = + storage.get ( + property.name, + serializer = serializer ?: storage.json.serializersModule.serializer ( + type = type ?: error("Contract error") + ) as KSerializer, + default + ) + + operator fun setValue(storage: KBlockingDataStorage, property: KProperty<*>, value: T) = + storage.set ( + property.name, + serializer = serializer ?: storage.json.serializersModule.serializer ( + type = type ?: error("Contract error") + ) as KSerializer, + value + ) +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSProperty.kt b/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSProperty.kt new file mode 100644 index 0000000..b2e4ea9 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDSProperty.kt @@ -0,0 +1,40 @@ +package `fun`.kotlingang.kds.delegate + +import `fun`.kotlingang.kds.KBlockingDataStorage +import `fun`.kotlingang.kds.annotation.DelicateKDSApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer +import kotlin.reflect.KProperty + + +@Suppress("unused") +fun KBlockingDataStorage.property(serializer: KSerializer, default: () -> T) = + KDSPropertyProvider(storage = this, serializer, default) + +inline fun KBlockingDataStorage.property(noinline default: () -> T) = + KDSPropertyProvider(storage = this, json.serializersModule.serializer(), default) + +class KDSPropertyProvider ( + private val storage: KBlockingDataStorage, + private val serializer: KSerializer, + private val default: () -> T +) { + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = + KDSProperty(storage, property.name, serializer, default) +} + +@OptIn(DelicateKDSApi::class) +class KDSProperty ( + private val storage: KBlockingDataStorage, + private val name: String, + private val serializer: KSerializer, + private val default: () -> T +) { + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = + storage.set(name = property.name, serializer, value) + + operator fun getValue(thisRef: Any?, property: KProperty<*>) = + storage.get(name = property.name, serializer, default) + + fun clear() = storage.clear(name) +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDataStoragePropertyDelegate.kt b/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDataStoragePropertyDelegate.kt deleted file mode 100644 index dd26991..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/delegate/KDataStoragePropertyDelegate.kt +++ /dev/null @@ -1,55 +0,0 @@ -@file:Suppress("UNCHECKED_CAST") - -package `fun`.kotlingang.kds.delegate - -import `fun`.kotlingang.kds.KDataStorage -import `fun`.kotlingang.kds.withLock -import kotlinx.serialization.KSerializer -import kotlin.reflect.KProperty - - -class KDataStorageProperty internal constructor( - private val serializer: KSerializer, - private val lazyDefault: () -> T -) { - - private var delegated: Pair>? = null - - fun clear() { - val (storage, property) = delegated ?: error("There is no property that was delegated") - storage.clear(property.name) - } - - operator fun provideDelegate(storage: KDataStorage, property: KProperty<*>): KDataStoragePropertyDelegate { - delegated = storage to property - return KDataStoragePropertyDelegate(serializer, lazyDefault) - } -} - -class KDataStoragePropertyDelegate internal constructor ( - private val serializer: KSerializer, - private val lazyDefault: () -> T -) { - operator fun getValue(storage: KDataStorage, property: KProperty<*>): T = storage.blockingLocker.withLock { - val (exists, reference) = storage.getReference(property.name) - - // If there is reference then value was already gotten with this method - // and we should return local copy for case if value is mutable and it was changed - return if(exists) { - reference as T - } else { - val element = storage.data[property.name] - val value = element?.let { storage.json.decodeFromJsonElement(serializer, element) } ?: lazyDefault() - storage.saveReference(property.name, value, serializer) - storage.launchCommit() - value - } - } - - operator fun setValue(storage: KDataStorage, property: KProperty<*>, value: T) { - storage.blockingLocker.withLock { - storage.saveReference(property.name, value, serializer) - storage.launchCommit() - } - } -} diff --git a/utils/src/commonMain/kotlin/fun/kotlingang/kds/AnyUtils.kt b/src/commonMain/kotlin/fun/kotlingang/kds/extensions/any/unit.kt similarity index 52% rename from utils/src/commonMain/kotlin/fun/kotlingang/kds/AnyUtils.kt rename to src/commonMain/kotlin/fun/kotlingang/kds/extensions/any/unit.kt index 60e7e30..16f2f0e 100644 --- a/utils/src/commonMain/kotlin/fun/kotlingang/kds/AnyUtils.kt +++ b/src/commonMain/kotlin/fun/kotlingang/kds/extensions/any/unit.kt @@ -1,4 +1,4 @@ -package `fun`.kotlingang.kds +package `fun`.kotlingang.kds.extensions.any @Suppress("unused") diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/manager/DataManager.kt b/src/commonMain/kotlin/fun/kotlingang/kds/manager/DataManager.kt new file mode 100644 index 0000000..b225a98 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/manager/DataManager.kt @@ -0,0 +1,11 @@ +package `fun`.kotlingang.kds.manager + +interface BlockingDataManager { + fun loadDataBlocking(): String + fun saveDataBlocking(text: String) +} + +interface AsyncDataManager : BlockingDataManager { + suspend fun loadData(): String + suspend fun saveData(text: String) +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutate.kt b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutate.kt new file mode 100644 index 0000000..34c50dc --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutate.kt @@ -0,0 +1,12 @@ +package `fun`.kotlingang.kds.mutate + +import `fun`.kotlingang.kds.KAsyncDataStorage +import `fun`.kotlingang.kds.annotation.DelicateKDSApi + + +@OptIn(DelicateKDSApi::class) +inline fun KAsyncDataStorage.mutate(crossinline block: () -> Unit) { + withoutSave(block) + applyMutations() + launchCommit() +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateBlocking.kt b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateBlocking.kt new file mode 100644 index 0000000..b22f0f3 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateBlocking.kt @@ -0,0 +1,12 @@ +package `fun`.kotlingang.kds.mutate + +import `fun`.kotlingang.kds.KBlockingDataStorage +import `fun`.kotlingang.kds.annotation.DelicateKDSApi + + +@OptIn(DelicateKDSApi::class) +inline fun KBlockingDataStorage.mutateBlocking(crossinline block: () -> Unit) { + withoutSave(block) + applyMutations() + commitBlocking() +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateCommit.kt b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateCommit.kt new file mode 100644 index 0000000..881895d --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/mutate/mutateCommit.kt @@ -0,0 +1,12 @@ +package `fun`.kotlingang.kds.mutate + +import `fun`.kotlingang.kds.KAsyncDataStorage +import `fun`.kotlingang.kds.annotation.DelicateKDSApi + + +@OptIn(DelicateKDSApi::class) +suspend inline fun KAsyncDataStorage.mutateCommit(crossinline block: () -> Unit) { + withoutSave(block) + applyMutations() + commit() +} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt b/src/commonMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt deleted file mode 100644 index 81d7d43..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt +++ /dev/null @@ -1,9 +0,0 @@ -package `fun`.kotlingang.kds.storage - - -internal expect class BaseStorage(path: String) { - val path: String - - suspend fun saveStorage(text: String) - suspend fun loadStorage(): String? -} diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt b/src/commonMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt deleted file mode 100644 index f917490..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt +++ /dev/null @@ -1,5 +0,0 @@ -package `fun`.kotlingang.kds.storage - - -expect val dirPath: String -expect fun String.joinPath(path: String): String diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt b/src/commonMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt new file mode 100644 index 0000000..1794fec --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt @@ -0,0 +1,5 @@ +package `fun`.kotlingang.kds.sync + + +@PublishedApi +internal expect inline fun platformSynchronized(lock: Any, block: () -> R): R diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/test/launchTest.kt b/src/commonMain/kotlin/fun/kotlingang/kds/test/launchTest.kt new file mode 100644 index 0000000..292a0a1 --- /dev/null +++ b/src/commonMain/kotlin/fun/kotlingang/kds/test/launchTest.kt @@ -0,0 +1,9 @@ +package `fun`.kotlingang.kds.test + +import kotlinx.coroutines.CoroutineScope + + +/** + * Has only guarantee that [block] will be invoked, but no guarantee when + */ +expect fun CoroutineScope.launchTest(block: suspend CoroutineScope.() -> Unit) diff --git a/src/commonMain/kotlin/fun/kotlingang/kds/value/KDataValue.kt b/src/commonMain/kotlin/fun/kotlingang/kds/value/KDataValue.kt deleted file mode 100644 index 362a694..0000000 --- a/src/commonMain/kotlin/fun/kotlingang/kds/value/KDataValue.kt +++ /dev/null @@ -1,50 +0,0 @@ -@file:Suppress("UNCHECKED_CAST", "FunctionName") - -package `fun`.kotlingang.kds.value - -import `fun`.kotlingang.kds.KDataStorage -import `fun`.kotlingang.kds.StorageConfigBuilder -import kotlinx.coroutines.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer -import kotlin.reflect.KProperty -import kotlin.reflect.KType -import kotlin.reflect.typeOf - - -@OptIn(ExperimentalStdlibApi::class) -inline fun KValueStorage ( - default: T, - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope + SupervisorJob(), - noinline builder: StorageConfigBuilder = {} -) = KStoredValue(default, typeOf(), coroutineScope, builder) - - -class KStoredValue internal constructor ( - default: T, - serializer: KSerializer?, - type: KType?, - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope + SupervisorJob(), - builder: StorageConfigBuilder = {} -) : KDataStorage(name = "value", coroutineScope, builder) { - - constructor ( - default: T, - serializer: KSerializer, - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope + SupervisorJob(), - builder: StorageConfigBuilder = {} - ) : this(default, serializer, type = null, coroutineScope, builder) - - constructor ( - default: T, - type: KType, - coroutineScope: CoroutineScope = @OptIn(DelicateCoroutinesApi::class) GlobalScope + SupervisorJob(), - builder: StorageConfigBuilder = {} - ) : this(default, serializer = null, type, coroutineScope, builder) - - private val serializer = serializer ?: json.serializersModule.serializer(type!!) as KSerializer - - private var value by property(this.serializer, default) - - operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = ::value -} diff --git a/src/commonTest/kotlin/StorageTests.kt b/src/commonTest/kotlin/StorageTests.kt deleted file mode 100644 index 674ff15..0000000 --- a/src/commonTest/kotlin/StorageTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -import `fun`.kotlingang.kds.KDataStorage -import `fun`.kotlingang.kds.commitMutate -import `fun`.kotlingang.kds.mutate -import `fun`.kotlingang.kds.runTestBlocking -import kotlinx.coroutines.* -import kotlin.random.Random -import kotlin.test.Test - - -@DelicateCoroutinesApi -class StorageTests { - object Storage : KDataStorage(name = "Storage") { // or KDataStorage("name") or KDataStorage({ path("...") }) - var random by property { Random.nextLong() } - - val random2Delegate = property { Random.nextInt() } - var random2 by random2Delegate - - var launchesCount by property(0) - var list by property(mutableListOf()) - val mutableList by property(mutableListOf()) - } - - @Test - fun simpleStorageTest() = GlobalScope.runTestBlocking { - println("Awaiting loading") - Storage.commitMutate { - println("Awaited loading") - - println("Launches count: ${++launchesCount}") - println("Random value: $random") - list += "Element" - println("List saved") - - println("List: $list") - } - } - - @Test - fun mutableStorageTest() = GlobalScope.runTestBlocking { - with(Storage) { - awaitLoading() // for JS - mutableList += "Test" - launchCommit() - } - } - - @Test - fun clearTest() = GlobalScope.runTestBlocking { - with(Storage) { - println(random2) - random2Delegate.clear() - println(random2) - } - } -} diff --git a/src/commonTest/kotlin/ValueStorageTests.kt b/src/commonTest/kotlin/ValueStorageTests.kt deleted file mode 100644 index f63d1c6..0000000 --- a/src/commonTest/kotlin/ValueStorageTests.kt +++ /dev/null @@ -1,19 +0,0 @@ -import `fun`.kotlingang.kds.value.KValueStorage -import `fun`.kotlingang.kds.runTestBlocking -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlin.test.Test - - -@DelicateCoroutinesApi -class ValueStorageTests { - @Test - fun simpleValueStorageTest() = GlobalScope.runTestBlocking { - val valueStorage = KValueStorage(default = 0, coroutineScope = this) - var launchNumber by valueStorage - - // Required for JS targets - valueStorage.awaitLoading() - println(++launchNumber) - } -} diff --git a/src/filesTarget/kotlin/fun/kotlingang/kds/KFileDataStorage.kt b/src/filesTarget/kotlin/fun/kotlingang/kds/KFileDataStorage.kt new file mode 100644 index 0000000..99e00f7 --- /dev/null +++ b/src/filesTarget/kotlin/fun/kotlingang/kds/KFileDataStorage.kt @@ -0,0 +1,39 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package `fun`.kotlingang.kds + +import `fun`.kotlingang.kds.files.File +import `fun`.kotlingang.kds.files.Files +import `fun`.kotlingang.kds.manager.FileDataManager +import kotlinx.coroutines.* +import kotlinx.serialization.json.Json + + +typealias KFileDataStorage = KAsyncDataStorage + +fun KFileDataStorage ( + absolutePath: String, + json: Json = Json, + scope: CoroutineScope = GlobalScope + SupervisorJob() + CoroutineName("KDS Coroutine") +): KFileDataStorage = KFileDataStorage ( + json, scope, + FileDataManager(File(absolutePath)) +) + +fun KFileDataStorage ( + name: String, + json: Json = Json, + scope: CoroutineScope = GlobalScope + SupervisorJob() + CoroutineName("KDS Coroutine"), + @Suppress("UNUSED_PARAMETER") + unused: Nothing? = null +): KFileDataStorage = KFileDataStorage ( + json, scope, + FileDataManager(Files.homeDir.join(path = "data").join(path = "$name.json")) +) + +fun KFileDataStorage ( + json: Json = Json, + scope: CoroutineScope = GlobalScope + SupervisorJob() + CoroutineName("KDS Coroutine") +): KFileDataStorage = KFileDataStorage ( + json, scope, FileDataManager(Files.homeDir.join(path = "data").join(path = "data.json")) +) diff --git a/src/filesTarget/kotlin/fun/kotlingang/kds/files/Files.kt b/src/filesTarget/kotlin/fun/kotlingang/kds/files/Files.kt new file mode 100644 index 0000000..255801b --- /dev/null +++ b/src/filesTarget/kotlin/fun/kotlingang/kds/files/Files.kt @@ -0,0 +1,11 @@ +package `fun`.kotlingang.kds.files + + +expect object Files { + val homeDir: File +} + +expect class File(path: String) { + val path: String + fun join(path: String): File +} diff --git a/src/filesTarget/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt b/src/filesTarget/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt new file mode 100644 index 0000000..1824585 --- /dev/null +++ b/src/filesTarget/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt @@ -0,0 +1,6 @@ +package `fun`.kotlingang.kds.manager + +import `fun`.kotlingang.kds.files.File + + +internal expect class FileDataManager(file: File) : AsyncDataManager diff --git a/src/jsMain/kotlin/fun/kotlingang/kds/KLocalDataStorage.kt b/src/jsMain/kotlin/fun/kotlingang/kds/KLocalDataStorage.kt new file mode 100644 index 0000000..7bbaf11 --- /dev/null +++ b/src/jsMain/kotlin/fun/kotlingang/kds/KLocalDataStorage.kt @@ -0,0 +1,10 @@ +package `fun`.kotlingang.kds + +import `fun`.kotlingang.kds.manager.BlockingDataManager +import `fun`.kotlingang.kds.manager.LocalStorageDataManager +import kotlinx.serialization.json.Json + + +typealias KLocalDataStorage = KBlockingDataStorage +fun KLocalDataStorage(json: Json = Json, key: String = "data"): KLocalDataStorage = + KBlockingDataStorage(json, LocalStorageDataManager(key)) diff --git a/src/jsMain/kotlin/fun/kotlingang/kds/manager/LocalStorageDataManager.kt b/src/jsMain/kotlin/fun/kotlingang/kds/manager/LocalStorageDataManager.kt new file mode 100644 index 0000000..b228033 --- /dev/null +++ b/src/jsMain/kotlin/fun/kotlingang/kds/manager/LocalStorageDataManager.kt @@ -0,0 +1,16 @@ +package `fun`.kotlingang.kds.manager + +import kotlinx.browser.localStorage +import org.w3c.dom.get +import org.w3c.dom.set + + +class LocalStorageDataManager ( + private val key: String +) : BlockingDataManager { + override fun loadDataBlocking() = localStorage[key] ?: "{}" + + override fun saveDataBlocking(text: String) { + localStorage[key] = text + } +} diff --git a/src/jsMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt b/src/jsMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt deleted file mode 100644 index 7dae523..0000000 --- a/src/jsMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt +++ /dev/null @@ -1,11 +0,0 @@ -package `fun`.kotlingang.kds.storage - -import kotlinx.browser.localStorage - - -internal actual class BaseStorage actual constructor(actual val path: String) { - - actual suspend fun saveStorage(text: String) = localStorage.setItem(path, text) - actual suspend fun loadStorage() = localStorage.getItem(path) - -} diff --git a/src/jsMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt b/src/jsMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt deleted file mode 100644 index ee534ea..0000000 --- a/src/jsMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt +++ /dev/null @@ -1,6 +0,0 @@ -package `fun`.kotlingang.kds.storage - -// Nothing because Browser js using localStorage for saving data -actual val dirPath = "" - -actual fun String.joinPath(path: String) = "$this/$path" diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/files/Files.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/files/Files.kt new file mode 100644 index 0000000..4ce6f41 --- /dev/null +++ b/src/jvmMain/kotlin/fun/kotlingang/kds/files/Files.kt @@ -0,0 +1,14 @@ +package `fun`.kotlingang.kds.files + +import java.io.File as JavaFile + + +actual object Files { + actual val homeDir = File(System.getProperty("user.dir")) +} + + +actual class File actual constructor(actual val path: String) { + private val selfPath = path + actual fun join(path: String) = File(JavaFile(selfPath, path).absolutePath) +} diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt new file mode 100644 index 0000000..9f07999 --- /dev/null +++ b/src/jvmMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt @@ -0,0 +1,46 @@ +package `fun`.kotlingang.kds.manager + +import `fun`.kotlingang.kds.files.File +import kotlinx.coroutines.* +import java.io.File as JavaFile + + +internal actual class FileDataManager actual constructor(file: File) : AsyncDataManager { + private val javaFile = JavaFile(file.path) + + init { + if(!javaFile.exists()) { + javaFile.parentFile?.mkdirs() + javaFile.createNewFile() + javaFile.writeText(text = "{}") + } + } + + override fun loadDataBlocking() = javaFile.readText() + override fun saveDataBlocking(text: String) = javaFile.writeText(text) + + private val bufferSize = DEFAULT_BUFFER_SIZE + + override suspend fun loadData(): String = withContext(Dispatchers.IO) { + buildString { + val buffer = CharArray(bufferSize) + javaFile.bufferedReader(bufferSize = bufferSize).use { reader -> + @Suppress("BlockingMethodInNonBlockingContext") + while(true) { + val readCount = reader.read(buffer).takeIf { it >= 0 } ?: return@use + append(buffer.slice(0 until readCount).joinToString(separator = "")) + yield() + } + } + } + } + + override suspend fun saveData(text: String) = withContext(Dispatchers.IO) { + javaFile.bufferedWriter(bufferSize = bufferSize).use { writer -> + text.chunked(size = bufferSize).forEach { text -> + writer.write(text) + yield() + } + } + } +} diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt deleted file mode 100644 index d946f10..0000000 --- a/src/jvmMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt +++ /dev/null @@ -1,26 +0,0 @@ -package `fun`.kotlingang.kds.storage - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import java.io.File -import java.io.IOException - - -@Suppress("BlockingMethodInNonBlockingContext", "CanBeParameter") -internal actual class BaseStorage actual constructor(actual val path: String) { - - private val file = File(path).apply { - try { parentFile.mkdirs() } catch (t: Throwable) {} - createNewFile() - } - - actual suspend fun saveStorage(text: String) = withContext(Dispatchers.IO) { - file.writeText(text) - } - - actual suspend fun loadStorage(): String? = withContext(Dispatchers.IO) { - file.readText().takeIf(String::isNotBlank) - } -} diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt deleted file mode 100644 index fde6b94..0000000 --- a/src/jvmMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt +++ /dev/null @@ -1,8 +0,0 @@ -package `fun`.kotlingang.kds.storage - -import java.nio.file.Paths - - -actual val dirPath: String = System.getProperty("user.dir") - -actual fun String.joinPath(path: String) = Paths.get(this, path).toString() diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt new file mode 100644 index 0000000..bb41fca --- /dev/null +++ b/src/jvmMain/kotlin/fun/kotlingang/kds/sync/synchronized.kt @@ -0,0 +1,4 @@ +package `fun`.kotlingang.kds.sync + + +internal actual inline fun platformSynchronized(lock: Any, block: () -> R) = synchronized(lock) { block() } diff --git a/src/jvmMain/kotlin/fun/kotlingang/kds/test/launchTest.kt b/src/jvmMain/kotlin/fun/kotlingang/kds/test/launchTest.kt new file mode 100644 index 0000000..eb74c15 --- /dev/null +++ b/src/jvmMain/kotlin/fun/kotlingang/kds/test/launchTest.kt @@ -0,0 +1,10 @@ +package `fun`.kotlingang.kds.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + + +actual fun CoroutineScope.launchTest(block: suspend CoroutineScope.() -> Unit) = runBlocking { + launch { block() }.join() +} diff --git a/src/jvmTest/kotlin/StorageTests.kt b/src/jvmTest/kotlin/StorageTests.kt new file mode 100644 index 0000000..5e799cf --- /dev/null +++ b/src/jvmTest/kotlin/StorageTests.kt @@ -0,0 +1,59 @@ +import `fun`.kotlingang.kds.KFileDataStorage +import `fun`.kotlingang.kds.delegate.KDSDelegate +import `fun`.kotlingang.kds.delegate.property +import `fun`.kotlingang.kds.mutate.mutate +import `fun`.kotlingang.kds.mutate.mutateCommit +import `fun`.kotlingang.kds.test.launchTest +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.serialization.builtins.serializer +import org.junit.Test +import kotlin.random.Random + + +val storage = KFileDataStorage(name = "data") + +val randomDelegate = storage.property { Random.nextLong() } +var random by randomDelegate + +var KFileDataStorage.random2 by KDSDelegate { Random.nextInt() } + +var KFileDataStorage.launchesCount by KDSDelegate(serializer = Int.serializer()) { 0 } + +var list by storage.property { mutableListOf() } +val mutableList by storage.property { mutableListOf() } + + +@DelicateCoroutinesApi +class StorageTests { + @Test + fun simpleStorageTest() = GlobalScope.launchTest { + with(storage) { + println("Awaiting loading") + storage.loadDataBlocking() + storage.loadData() + println("Awaited loading") + + println("Launches count: ${++launchesCount}") + println("Random value: $random2") + + println("ListBefore: $list") + + mutate { + list += "Element" + } + + println("List: $list") + storage.commit() + } + } + + @Test + fun mutableStorageTest() = GlobalScope.launchTest { + with(storage) { + mutateCommit { + mutableList += "Test" + } + } + } +} diff --git a/src/jvmTest/kotlin/StressTest.kt b/src/jvmTest/kotlin/StressTest.kt new file mode 100644 index 0000000..5344c33 --- /dev/null +++ b/src/jvmTest/kotlin/StressTest.kt @@ -0,0 +1,25 @@ +import `fun`.kotlingang.kds.delegate.property +import `fun`.kotlingang.kds.test.launchTest +import kotlinx.coroutines.* +import org.junit.Test + + +var stressTest by storage.property { "" } + +@DelicateCoroutinesApi +class StressTest { + @Test + fun stressTest() = GlobalScope.launchTest { + withContext(Dispatchers.IO) { + (1..1_000).map { i -> + async { + stressTest = "S".repeat(i) + println(i) + } + }.awaitAll() + } + storage.commit() + println("Committed: $stressTest (${stressTest.length})") + delay(2_000) + } +} diff --git a/src/nodeMain/kotlin/fun/kotlingang/kds/files/Files.kt b/src/nodeMain/kotlin/fun/kotlingang/kds/files/Files.kt new file mode 100644 index 0000000..7dabdef --- /dev/null +++ b/src/nodeMain/kotlin/fun/kotlingang/kds/files/Files.kt @@ -0,0 +1,13 @@ +package `fun`.kotlingang.kds.files + +import process +import path.path as pathModule + + +actual object Files { + actual val homeDir: File = File(process.cwd()) +} + +actual class File actual constructor(actual val path: String) { + actual fun join(path: String) = File(pathModule.join(path)) +} diff --git a/src/nodeMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt b/src/nodeMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt similarity index 55% rename from src/nodeMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt rename to src/nodeMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt index f021c07..a04a348 100644 --- a/src/nodeMain/kotlin/fun/kotlingang/kds/storage/BaseStorage.kt +++ b/src/nodeMain/kotlin/fun/kotlingang/kds/manager/FileDataManager.kt @@ -1,5 +1,6 @@ -package `fun`.kotlingang.kds.storage +package `fun`.kotlingang.kds.manager +import `fun`.kotlingang.kds.files.File import fs.MakeDirectoryOptions import kotlinx.coroutines.CompletableDeferred import path.path @@ -8,33 +9,35 @@ import path.path private val fs = js("require('fs')") private val pathModule = path +internal actual class FileDataManager actual constructor ( + file: File +) : AsyncDataManager { -internal actual class BaseStorage actual constructor(actual val path: String){ init { if(!fs.existsSync(path) as Boolean) { - val parent = pathModule.resolve(path, "..") - + val parent = pathModule.resolve(file.path, "..") val options = object : MakeDirectoryOptions { override var recursive: Boolean? = true } fs.mkdirSync(parent, options) - fs.appendFileSync(path, "{}") } } - actual suspend fun saveStorage(text: String) { - val deferred = CompletableDeferred() - fs.writeFile(path, text) { _ -> - deferred.complete(Unit) + override fun loadDataBlocking() = fs.readFileSync(path, "utf8") as String + override fun saveDataBlocking(text: String) = fs.writeFileSync(path, text) + + override suspend fun loadData(): String { + val deferred = CompletableDeferred() + fs.readFile(path, "utf8") { _, data -> + deferred.complete(data as String) } return deferred.await() } - - actual suspend fun loadStorage(): String? { - val deferred = CompletableDeferred() - fs.readFile(path, "utf8") { _, data -> - deferred.complete(data as? String) + override suspend fun saveData(text: String) { + val deferred = CompletableDeferred() + fs.writeFile(path, text) { _ -> + deferred.complete(Unit) } return deferred.await() } diff --git a/src/nodeMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt b/src/nodeMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt deleted file mode 100644 index cb378bc..0000000 --- a/src/nodeMain/kotlin/fun/kotlingang/kds/storage/StorageUtils.kt +++ /dev/null @@ -1,10 +0,0 @@ -package `fun`.kotlingang.kds.storage - -import process - - -private val pathModule = js("require('path')") - -actual val dirPath = process.cwd() - -actual fun String.joinPath(path: String) = pathModule.join(this, path) as String diff --git a/utils/build.gradle.kts b/utils/build.gradle.kts deleted file mode 100644 index 94cd113..0000000 --- a/utils/build.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE") - - -plugins { - kotlin(plugin.multiplatform) -} - -repositories { - mavenCentral() -} -kotlin { - js(IR) { - useCommonJs() - browser() - nodejs() - } - jvm() - - sourceSets { - val commonMain by getting { - dependencies { - implementation(coroutines) - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test-common")) - implementation(kotlin("test-annotations-common")) - } - } - val jvmTest by getting { - dependencies { - implementation(kotlin("test-junit")) - } - } - - all { - languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") - } - } -} diff --git a/utils/src/commonMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt b/utils/src/commonMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt deleted file mode 100644 index c0fd6a1..0000000 --- a/utils/src/commonMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package `fun`.kotlingang.kds - -import kotlinx.coroutines.CoroutineScope - - -/** - * @return true if current target can run coroutine blocking - */ -expect fun runBlockingPlatform(block: suspend CoroutineScope.() -> T): Pair - -expect fun CoroutineScope.runTestBlocking(block: suspend CoroutineScope.() -> Unit) - diff --git a/utils/src/commonMain/kotlin/fun/kotlingang/kds/PlatformLocker.kt b/utils/src/commonMain/kotlin/fun/kotlingang/kds/PlatformLocker.kt deleted file mode 100644 index 1612818..0000000 --- a/utils/src/commonMain/kotlin/fun/kotlingang/kds/PlatformLocker.kt +++ /dev/null @@ -1,33 +0,0 @@ -package `fun`.kotlingang.kds - -import kotlinx.coroutines.sync.Mutex - - -/** - * Spin locker based on coroutine mutex. - * Be careful and call it only from blocking functions. - */ -class PlatformLocker { - private val mutex = Mutex() - - fun lock() = runBlockingPlatform { - mutex.lock() - }.unit - - fun unlock() = runBlockingPlatform { - mutex.unlock() - }.unit -} - -/** - * To prevent infinity looping use only blocking methods inside [block] - */ -inline fun PlatformLocker.withLock(block: () -> R): R { - lock() - - try { - return block() - } finally { - unlock() - } -} diff --git a/utils/src/jsMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt b/utils/src/jsMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt deleted file mode 100644 index 2d92269..0000000 --- a/utils/src/jsMain/kotlin/fun/kotlingang/kds/CoroutineUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package `fun`.kotlingang.kds - -import kotlinx.coroutines.* - - -actual fun CoroutineScope.runTestBlocking(block: suspend CoroutineScope.() -> Unit) { - val promise = promise { - block() - } - eval("(promise) => async () => await promise")(promise)() -} - -/** - * @return true if current target can run coroutine blocking - */ -actual fun runBlockingPlatform(block: suspend CoroutineScope.() -> T): Pair = false to null diff --git a/utils/src/jvmMain/kotlin/fun/kotlingang/kds/JvmCoroutineUtils.kt b/utils/src/jvmMain/kotlin/fun/kotlingang/kds/JvmCoroutineUtils.kt deleted file mode 100644 index e14c0d4..0000000 --- a/utils/src/jvmMain/kotlin/fun/kotlingang/kds/JvmCoroutineUtils.kt +++ /dev/null @@ -1,14 +0,0 @@ -package `fun`.kotlingang.kds - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking - - -actual fun CoroutineScope.runTestBlocking(block: suspend CoroutineScope.() -> Unit) - = runBlocking(coroutineContext, block) - -/** - * @return true if current target can run coroutine blocking - */ -actual fun runBlockingPlatform(block: suspend CoroutineScope.() -> T): Pair - = runBlocking { block() }.let { true to it } diff --git a/utils/src/jvmTest/kotlin/LockTests.kt b/utils/src/jvmTest/kotlin/LockTests.kt deleted file mode 100644 index 25b97cc..0000000 --- a/utils/src/jvmTest/kotlin/LockTests.kt +++ /dev/null @@ -1,24 +0,0 @@ -import `fun`.kotlingang.kds.PlatformLocker -import `fun`.kotlingang.kds.withLock -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Test - -class LockerTests { - @Test - fun simpleTest() = runBlocking { - val locker = PlatformLocker() - launch { - locker.withLock { - println("First lock") - Thread.sleep(5000) - } - } - delay(1000) - locker.withLock { - println("Second lock") - } - Thread.sleep(10000) - } -} \ No newline at end of file