mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-31 12:10:29 +00:00 
			
		
		
		
	Compare commits
	
		
			78 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d69fee1732 | |||
| 178518db5e | |||
| 6fb20fb973 | |||
| 831bf44e34 | |||
| a4c6c367e3 | |||
| bc98e59709 | |||
| ef287bc331 | |||
| 3437f4c712 | |||
| 618f2dcd79 | |||
| 6df8ad3095 | |||
| eda6221288 | |||
| a9859f6a0d | |||
| 0db88bac25 | |||
| daa3d9c0dd | |||
| b343b33594 | |||
| 46e435a448 | |||
| 7fe62b4ffa | |||
| 9c94348a15 | |||
| bac256e93e | |||
| 49f59aa129 | |||
| 800dab5be0 | |||
| b9977527b2 | |||
| c216dba69d | |||
| d4148d52e3 | |||
| 2006a8cdd0 | |||
| feb52ecbd1 | |||
| 42909c3b7a | |||
| 706a787163 | |||
| 1bc14bded6 | |||
| f00cb81db1 | |||
| 2fbd14956d | |||
| 24657b43be | |||
| 659d3b6fa5 | |||
| 339483c8a3 | |||
| 51ec46bbd7 | |||
| 8a059cc26d | |||
| ddb8e1efb4 | |||
| 3bf2ed5168 | |||
| e70d34d91a | |||
| 7a650f5c2f | |||
| fc6f5ae2ee | |||
| 3b7dde3cb1 | |||
| 0a5cfaba18 | |||
| ea527b5e91 | |||
| 85f11439e8 | |||
| 98c7b48625 | |||
| 8b007bb3af | |||
| 89e2d88d1c | |||
| 260399e965 | |||
| de72843b8e | |||
| b589142d9f | |||
| f479c85869 | |||
| 26992c039a | |||
| ef50e1a24f | |||
| 2f201670d2 | |||
| 90c80573a0 | |||
| 059519cdca | |||
| 55647e2e2f | |||
| 1802be68ef | |||
| aa79496a36 | |||
| 4bfa4c32aa | |||
| d8ca29eab1 | |||
| f7a9f2e13d | |||
| af286d3d53 | |||
| 9a20e8155e | |||
| b81af7e1c8 | |||
| 74563bbac9 | |||
| 07ad79f0b3 | |||
| 5c28255e06 | |||
| 99783f281d | |||
| a5008c3d15 | |||
| f807f2beeb | |||
| db34b25ef2 | |||
| 96daf11fd4 | |||
| f7e98dfd2d | |||
| 61277e92bd | |||
| 32ef9f399f | |||
| 54e6ca5dc3 | 
							
								
								
									
										80
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,85 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 0.25.2 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |   * `Exposed`: `0.59.0` -> `0.60.0` | ||||||
|  | * `Repo`: | ||||||
|  |   * `Cache`: | ||||||
|  |     * Add extensions `alsoInvalidate`, `alsoInvalidateAsync`, `alsoInvalidateSync` and `alsoInvalidateSyncLogging` | ||||||
|  | * `Koin`: | ||||||
|  |   * Add extensions `singleSuspend` and `factorySuspend` for defining of dependencies with suspendable blocks | ||||||
|  |  | ||||||
|  | ## 0.25.1 | ||||||
|  |  | ||||||
|  | * `Coroutines`: | ||||||
|  |   * Add `SortedMapLikeBinaryTreeNode` | ||||||
|  | * `Pagination`: | ||||||
|  |   * `Compose`: | ||||||
|  |     * One more rework of `InfinityPagedComponent` and `PagedComponent` | ||||||
|  |  | ||||||
|  | ## 0.25.0 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |   * `Cache`: | ||||||
|  |     * All cache repos now do not have `open` vals - to avoid collisions in runtime | ||||||
|  |  | ||||||
|  | ## 0.24.9 | ||||||
|  |  | ||||||
|  | * `Pagination`: | ||||||
|  |   * Make alternative constructor parameter `size` of `PaginationResult` with default value | ||||||
|  |   * Add `Pagination.previousPage` extension | ||||||
|  |   * `Compose`: | ||||||
|  |     * Rework of `InfinityPagedComponentContext` | ||||||
|  |     * Rework of `PagedComponent` | ||||||
|  |  | ||||||
|  | ## 0.24.8 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |   * `Ktor`: `3.1.0` -> `3.1.1` | ||||||
|  |   * `KSP`: `2.1.10-1.0.30` -> `2.1.10-1.0.31` | ||||||
|  | * `Common`: | ||||||
|  |   * `Compose`: | ||||||
|  |     * Add component `LoadableComponent` | ||||||
|  | * `Coroutines`: | ||||||
|  |   * Add `SortedBinaryTreeNode` | ||||||
|  | * `Pagination`: | ||||||
|  |   * `Compose`: | ||||||
|  |     * Add components `PagedComponent` and `InfinityPagedComponent` | ||||||
|  |  | ||||||
|  | ## 0.24.7 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |   * `SQLite`: `3.49.0.0` -> `3.49.1.0` | ||||||
|  | * `Common`: | ||||||
|  |   * Add `retryOnFailure` utility for simple retries code writing | ||||||
|  | * `Repos`: | ||||||
|  |   * `Cache`: | ||||||
|  |     * Fix of `FullKeyValueCacheRepo` fields usage | ||||||
|  |   * `Exposed`: | ||||||
|  |     * `AbstractExposedKeyValuesRepo` will produce `onValueRemoved` event on `set` if some data has been removed | ||||||
|  |  | ||||||
|  | ## 0.24.6 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |   * `KSLog`: `1.4.0` -> `1.4.1` | ||||||
|  |   * `Exposed`: `0.58.0` -> `0.59.0` | ||||||
|  |   * `SQLite`: `3.48.0.0` -> `3.49.0.0` | ||||||
|  |   * `AndroidFragment`: `1.8.5` -> `1.8.6` | ||||||
|  | * `Coroutines`: | ||||||
|  |   * Safely functions has been replaced with `Logging` variations (resolve of [#541](https://github.com/InsanusMokrassar/MicroUtils/issues/541)) | ||||||
|  | * `KSP`: | ||||||
|  |   * `Variations`: | ||||||
|  |     * Module has been created | ||||||
|  |  | ||||||
|  | ## 0.24.5 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |   * `Kotlin`: `2.1.0` -> `2.1.10` | ||||||
|  |   * `SQLite`: `3.47.2.0` -> `3.48.0.0` | ||||||
|  |   * `Koin`: `4.0.1` -> `4.0.2` | ||||||
|  |   * `Android RecyclerView`: `1.3.2` -> `1.4.0` | ||||||
|  |  | ||||||
| ## 0.24.4 | ## 0.24.4 | ||||||
|  |  | ||||||
| * `Repos`: | * `Repos`: | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ kotlin { | |||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api project(":micro_utils.common") |                 api project(":micro_utils.common") | ||||||
|  |                 api libs.kt.coroutines | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,74 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.Optional | ||||||
|  | import dev.inmo.micro_utils.common.dataOrThrow | ||||||
|  | import dev.inmo.micro_utils.common.optional | ||||||
|  |  | ||||||
|  | class LoadableComponentContext<T> internal constructor( | ||||||
|  |     presetOptional: Optional<T>, | ||||||
|  | ) { | ||||||
|  |     internal val iterationState: MutableState<Int> = mutableStateOf(0) | ||||||
|  |  | ||||||
|  |     internal var dataOptional: Optional<T> = if (presetOptional.dataPresented) presetOptional else Optional.absent() | ||||||
|  |         private set | ||||||
|  |     internal val dataState: MutableState<Optional<T>> = mutableStateOf(dataOptional) | ||||||
|  |  | ||||||
|  |     fun reload() { | ||||||
|  |         iterationState.value++ | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     preload: Optional<T>, | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     val context = remember { LoadableComponentContext(preload) } | ||||||
|  |  | ||||||
|  |     LaunchedEffect(context.iterationState.value) { | ||||||
|  |         context.dataState.value = loader(context).optional | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     context.dataState.let { | ||||||
|  |         if (it.value.dataPresented) { | ||||||
|  |             context.block(it.value.dataOrThrow(IllegalStateException("Data must be presented, but optional has been changed by some way"))) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     preload: T, | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     LoadableComponent(preload.optional, loader, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     LoadableComponent(Optional.absent(), loader, block) | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.test.ExperimentalTestApi | ||||||
|  | import androidx.compose.ui.test.runComposeUiTest | ||||||
|  | import dev.inmo.micro_utils.common.compose.LoadableComponent | ||||||
|  | import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
|  | import kotlinx.coroutines.flow.filter | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.flow.firstOrNull | ||||||
|  | import org.jetbrains.annotations.TestOnly | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  |  | ||||||
|  | class LoadableComponentTests { | ||||||
|  |     @OptIn(ExperimentalTestApi::class) | ||||||
|  |     @Test | ||||||
|  |     @TestOnly | ||||||
|  |     fun testSimpleLoad() = runComposeUiTest { | ||||||
|  |         val loadingFlow = SpecialMutableStateFlow<Int>(0) | ||||||
|  |         val loadedFlow = SpecialMutableStateFlow<Int>(0) | ||||||
|  |         setContent { | ||||||
|  |             LoadableComponent<Int>({ | ||||||
|  |                 loadingFlow.filter { it == 1 }.first() | ||||||
|  |             }) { | ||||||
|  |                 assert(dataState.value.data == 1) | ||||||
|  |                 remember { | ||||||
|  |                     loadedFlow.value = 2 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertTrue(loadedFlow.value == 0) | ||||||
|  |  | ||||||
|  |         loadingFlow.value = 1 | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertTrue(loadedFlow.value == 2) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates simple [Comparator] which will use [compareTo] of [T] for both objects | ||||||
|  |  */ | ||||||
|  | fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will try to execute [action] and, if any exception will happen, execution will be retried. | ||||||
|  |  * This process will happen at most [count] times. There is no any limits on [count] value, but [action] will run at | ||||||
|  |  * least once and [retryOnFailure] will return its result if it is successful | ||||||
|  |  */ | ||||||
|  | inline fun <T> retryOnFailure(count: Int, action: () -> T): T { | ||||||
|  |     var triesCount = 0 | ||||||
|  |     while (true) { | ||||||
|  |         val result = runCatching { | ||||||
|  |             action() | ||||||
|  |         }.onFailure { | ||||||
|  |             triesCount++ | ||||||
|  |  | ||||||
|  |             if (triesCount >= count) { | ||||||
|  |                 throw it | ||||||
|  |             } else { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (result.isSuccess) return result.getOrThrow() | ||||||
|  |     } | ||||||
|  |     error("Unreachable code: retry must throw latest exception if error happen or success value if not") | ||||||
|  | } | ||||||
| @@ -11,6 +11,7 @@ kotlin { | |||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.coroutines |                 api libs.kt.coroutines | ||||||
|  |                 api libs.kslog | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         jsMain { |         jsMain { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| package dev.inmo.micro_utils.coroutines | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.kslog.common.KSLog | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
| import kotlinx.coroutines.sync.Mutex | import kotlinx.coroutines.sync.Mutex | ||||||
| @@ -16,6 +17,45 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend | |||||||
|  * Use [subscribe], but all [block]s will be called inside of [safely] function. |  * Use [subscribe], but all [block]s will be called inside of [safely] function. | ||||||
|  * Use [onException] to set up your reaction for [Throwable]s |  * Use [onException] to set up your reaction for [Throwable]s | ||||||
|  */ |  */ | ||||||
|  | inline fun <T> Flow<T>.subscribeLogging( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribe(scope) { | ||||||
|  |     it.runCatchingLogging( | ||||||
|  |         errorMessageBuilder, | ||||||
|  |         logger | ||||||
|  |     ) { | ||||||
|  |         block(it) | ||||||
|  |     }.getOrThrow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler] | ||||||
|  |  */ | ||||||
|  | inline fun <T> Flow<T>.subscribeLoggingDropExceptions( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribe(scope) { | ||||||
|  |     it.runCatchingLogging( | ||||||
|  |         errorMessageBuilder, | ||||||
|  |         logger | ||||||
|  |     ) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Use [subscribe], but all [block]s will be called inside of [safely] function. | ||||||
|  |  * Use [onException] to set up your reaction for [Throwable]s | ||||||
|  |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "Will be removed soon due to replacement by subscribeLogging", | ||||||
|  |     ReplaceWith("this.subscribeLogging(scope = scope, block = block)") | ||||||
|  | ) | ||||||
| inline fun <T> Flow<T>.subscribeSafely( | inline fun <T> Flow<T>.subscribeSafely( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||||
| @@ -29,6 +69,10 @@ inline fun <T> Flow<T>.subscribeSafely( | |||||||
| /** | /** | ||||||
|  * Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler] |  * Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler] | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "Will be removed soon due to replacement by subscribeLoggingDropExceptions", | ||||||
|  |     ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)") | ||||||
|  | ) | ||||||
| inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
| @@ -42,6 +86,10 @@ inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | |||||||
| /** | /** | ||||||
|  * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped |  * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "Will be removed soon due to replacement by subscribeLoggingDropExceptions", | ||||||
|  |     ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)") | ||||||
|  | ) | ||||||
| inline fun <T> Flow<T>.subscribeSafelySkippingExceptions( | inline fun <T> Flow<T>.subscribeSafelySkippingExceptions( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     noinline block: suspend (T) -> Unit |     noinline block: suspend (T) -> Unit | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ private class SubscribeAsyncReceiver<T>( | |||||||
|         get() = dataChannel |         get() = dataChannel | ||||||
|  |  | ||||||
|     init { |     init { | ||||||
|         scope.launchSafelyWithoutExceptions { |         scope.launchLoggingDropExceptions { | ||||||
|             for (data in dataChannel) { |             for (data in dataChannel) { | ||||||
|                 output(data) |                 output(data) | ||||||
|             } |             } | ||||||
| @@ -71,7 +71,7 @@ fun <T, M> Flow<T>.subscribeAsync( | |||||||
|         it.invoke(markersMap) |         it.invoke(markersMap) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     val job = subscribeSafelyWithoutExceptions(subscope) { data -> |     val job = subscribeLoggingDropExceptions(subscope) { data -> | ||||||
|         val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> |         val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> | ||||||
|             actor.send( |             actor.send( | ||||||
|                 AsyncSubscriptionCommandClearReceiver(marker) |                 AsyncSubscriptionCommandClearReceiver(marker) | ||||||
|   | |||||||
| @@ -15,6 +15,10 @@ import kotlin.coroutines.coroutineContext | |||||||
|  * |  * | ||||||
|  * @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution |  * @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }") | ||||||
|  | ) | ||||||
| suspend inline fun <T> runCatchingSafely( | suspend inline fun <T> runCatchingSafely( | ||||||
|     onException: ExceptionHandler<T>, |     onException: ExceptionHandler<T>, | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
| @@ -29,6 +33,10 @@ suspend inline fun <T> runCatchingSafely( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }") | ||||||
|  | ) | ||||||
| suspend inline fun <T, R> R.runCatchingSafely( | suspend inline fun <T, R> R.runCatchingSafely( | ||||||
|     onException: ExceptionHandler<T>, |     onException: ExceptionHandler<T>, | ||||||
|     block: suspend R.() -> T |     block: suspend R.() -> T | ||||||
| @@ -39,10 +47,18 @@ suspend inline fun <T, R> R.runCatchingSafely( | |||||||
| /** | /** | ||||||
|  * Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter |  * Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }") | ||||||
|  | ) | ||||||
| suspend inline fun <T> runCatchingSafely( | suspend inline fun <T> runCatchingSafely( | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
| ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }") | ||||||
|  | ) | ||||||
| suspend inline fun <T, R> R.runCatchingSafely( | suspend inline fun <T, R> R.runCatchingSafely( | ||||||
|     block: suspend R.() -> T |     block: suspend R.() -> T | ||||||
| ): Result<T> = runCatchingSafely<T> { | ): Result<T> = runCatchingSafely<T> { | ||||||
| @@ -73,6 +89,9 @@ suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExce | |||||||
|  * After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call |  * After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call | ||||||
|  * [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block |  * [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  | ) | ||||||
| suspend fun <T> safelyWithContextExceptionHandler( | suspend fun <T> safelyWithContextExceptionHandler( | ||||||
|     contextExceptionHandler: ExceptionHandler<Unit>, |     contextExceptionHandler: ExceptionHandler<Unit>, | ||||||
|     safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler, |     safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||||
| @@ -94,6 +113,10 @@ suspend fun <T> safelyWithContextExceptionHandler( | |||||||
|  * |  * | ||||||
|  * @see runCatchingSafely |  * @see runCatchingSafely | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrThrow()") | ||||||
|  | ) | ||||||
| suspend inline fun <T> safely( | suspend inline fun <T> safely( | ||||||
|     onException: ExceptionHandler<T>, |     onException: ExceptionHandler<T>, | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
| @@ -104,9 +127,17 @@ suspend inline fun <T> safely( | |||||||
|  * |  * | ||||||
|  * @see runCatchingSafely |  * @see runCatchingSafely | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()") | ||||||
|  | ) | ||||||
| suspend inline fun <T> safely( | suspend inline fun <T> safely( | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
| ): T = safely(defaultSafelyExceptionHandler, block) | ): T = safely(defaultSafelyExceptionHandler, block) | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()") | ||||||
|  | ) | ||||||
| suspend inline fun <T, R> R.safely( | suspend inline fun <T, R> R.safely( | ||||||
|     block: suspend R.() -> T |     block: suspend R.() -> T | ||||||
| ): T = safely<T> { block() } | ): T = safely<T> { block() } | ||||||
| @@ -137,11 +168,19 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = { | |||||||
|  * Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of |  * Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of | ||||||
|  * result from exception (instead of throwing it, by default always returns null) |  * result from exception (instead of throwing it, by default always returns null) | ||||||
|  */ |  */ | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()") | ||||||
|  | ) | ||||||
| suspend fun <T> safelyWithoutExceptions( | suspend fun <T> safelyWithoutExceptions( | ||||||
|     onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, |     onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
| ): T? = runCatchingSafely(onException, block).getOrNull() | ): T? = runCatchingSafely(onException, block).getOrNull() | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This function become redundant since coroutines correctly handling throwing exceptions", | ||||||
|  |     replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()") | ||||||
|  | ) | ||||||
| suspend fun <T> runCatchingSafelyWithoutExceptions( | suspend fun <T> runCatchingSafelyWithoutExceptions( | ||||||
|     onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler, |     onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler, | ||||||
|     block: suspend () -> T |     block: suspend () -> T | ||||||
|   | |||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.kslog.common.KSLog | ||||||
|  | import dev.inmo.kslog.common.e | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  | import kotlin.coroutines.EmptyCoroutineContext | ||||||
|  |  | ||||||
|  | fun CoroutineScope.launchLogging( | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> Unit | ||||||
|  | ) = launch(context, start) { | ||||||
|  |     runCatching { block() }.onFailure { | ||||||
|  |         logger.e(it) { errorMessageBuilder(it) } | ||||||
|  |     }.getOrThrow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun CoroutineScope.launchLoggingDropExceptions( | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> Unit | ||||||
|  | ) = launch(context, start) { | ||||||
|  |     runCatching { block() }.onFailure { | ||||||
|  |         logger.e(it) { errorMessageBuilder(it) } | ||||||
|  |     } // just dropping exception | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> CoroutineScope.asyncLogging( | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> T | ||||||
|  | ) = async(context, start) { | ||||||
|  |     runCatching { block() }.onFailure { | ||||||
|  |         logger.e(it) { errorMessageBuilder(it) } | ||||||
|  |     }.getOrThrow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> CoroutineScope.asyncLoggingDropExceptions( | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> T | ||||||
|  | ) = async(context, start) { | ||||||
|  |     runCatching { block() }.onFailure { | ||||||
|  |         logger.e(it) { errorMessageBuilder(it) } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,6 +4,10 @@ import kotlinx.coroutines.* | |||||||
| import kotlin.coroutines.CoroutineContext | import kotlin.coroutines.CoroutineContext | ||||||
| import kotlin.coroutines.EmptyCoroutineContext | import kotlin.coroutines.EmptyCoroutineContext | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This method will be removed soon. Use launchLogging instead", | ||||||
|  |     ReplaceWith("this.launchLogging(context = context, start = start, block = block)") | ||||||
|  | ) | ||||||
| fun CoroutineScope.launchSafely( | fun CoroutineScope.launchSafely( | ||||||
|     context: CoroutineContext = EmptyCoroutineContext, |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|     start: CoroutineStart = CoroutineStart.DEFAULT, |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
| @@ -15,6 +19,10 @@ fun CoroutineScope.launchSafely( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This method will be removed soon. Use launchLoggingDropExceptions instead", | ||||||
|  |     ReplaceWith("this.launchLoggingDropExceptions(context = context, start = start, block = block)") | ||||||
|  | ) | ||||||
| fun CoroutineScope.launchSafelyWithoutExceptions( | fun CoroutineScope.launchSafelyWithoutExceptions( | ||||||
|     context: CoroutineContext = EmptyCoroutineContext, |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|     start: CoroutineStart = CoroutineStart.DEFAULT, |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
| @@ -26,6 +34,10 @@ fun CoroutineScope.launchSafelyWithoutExceptions( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This method will be removed soon. Use asyncLogging instead", | ||||||
|  |     ReplaceWith("this.asyncLogging(context = context, start = start, block = block)") | ||||||
|  | ) | ||||||
| fun <T> CoroutineScope.asyncSafely( | fun <T> CoroutineScope.asyncSafely( | ||||||
|     context: CoroutineContext = EmptyCoroutineContext, |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|     start: CoroutineStart = CoroutineStart.DEFAULT, |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
| @@ -37,6 +49,10 @@ fun <T> CoroutineScope.asyncSafely( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @Deprecated( | ||||||
|  |     "This method will be removed soon. Use asyncLoggingDropExceptions instead", | ||||||
|  |     ReplaceWith("this.asyncLoggingDropExceptions(context = context, start = start, block = block)") | ||||||
|  | ) | ||||||
| fun <T> CoroutineScope.asyncSafelyWithoutExceptions( | fun <T> CoroutineScope.asyncSafelyWithoutExceptions( | ||||||
|     context: CoroutineContext = EmptyCoroutineContext, |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|     start: CoroutineStart = CoroutineStart.DEFAULT, |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.kslog.common.KSLog | ||||||
|  | import dev.inmo.kslog.common.e | ||||||
|  |  | ||||||
|  | inline fun <T, R> R.runCatchingLogging( | ||||||
|  |     noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  |     block: R.() -> T | ||||||
|  | ) = runCatching(block).onFailure { | ||||||
|  |     logger.e(it) { errorMessageBuilder(it) } | ||||||
|  | } | ||||||
| @@ -0,0 +1,318 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.collections | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
|  | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
|  | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
|  | import kotlinx.coroutines.job | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.coroutines.coroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates simple [Comparator] which will use [compareTo] of [T] for both objects | ||||||
|  |  */ | ||||||
|  | private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) } | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | class SortedBinaryTreeNode<T>( | ||||||
|  |     val data: T, | ||||||
|  |     internal val comparator: Comparator<T>, | ||||||
|  | ) : Iterable<SortedBinaryTreeNode<T>> { | ||||||
|  |     internal var leftNode: SortedBinaryTreeNode<T>? = null | ||||||
|  |     internal var rightNode: SortedBinaryTreeNode<T>? = null | ||||||
|  |     internal val locker: SmartRWLocker by lazy { | ||||||
|  |         SmartRWLocker() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getLeftNode() = locker.withReadAcquire { | ||||||
|  |         leftNode | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getRightNode() = locker.withReadAcquire { | ||||||
|  |         rightNode | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getLeft() = getLeftNode() ?.data | ||||||
|  |  | ||||||
|  |     suspend fun getRight() = getRightNode() ?.data | ||||||
|  |  | ||||||
|  |     override fun equals(other: Any?): Boolean { | ||||||
|  |         return other === this || (other is SortedBinaryTreeNode<*> && other.data == data && other.rightNode == rightNode && other.leftNode == leftNode) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun hashCode(): Int { | ||||||
|  |         return data.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun size(): Int { | ||||||
|  |         return locker.withReadAcquire { | ||||||
|  |             1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This [Iterator] will run from less to greater values of nodes starting the | ||||||
|  |      * [dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode]-receiver. Due to non-suspending | ||||||
|  |      * nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It | ||||||
|  |      * means, that tree can be changed during to iteration process | ||||||
|  |      */ | ||||||
|  |     override fun iterator(): Iterator<SortedBinaryTreeNode<T>> = iterator { | ||||||
|  |         leftNode ?.let { | ||||||
|  |             it.iterator().forEach { yield(it) } | ||||||
|  |         } | ||||||
|  |         yield(this@SortedBinaryTreeNode) | ||||||
|  |         rightNode ?.let { | ||||||
|  |             it.iterator().forEach { yield(it) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun toString(): String { | ||||||
|  |         return "$data($leftNode;$rightNode)" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         operator fun <T : Comparable<T>> invoke( | ||||||
|  |             data: T, | ||||||
|  |         ) = SortedBinaryTreeNode( | ||||||
|  |             data, | ||||||
|  |             data.createComparator() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will add subnode in tree if there are no any node with [newData] | ||||||
|  |  * | ||||||
|  |  * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, | ||||||
|  |  * will be used [SortedBinaryTreeNode.rightNode] | ||||||
|  |  * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently | ||||||
|  |  * checking node - will be returned currently checking node | ||||||
|  |  * * If [newData] is less than [SortedBinaryTreeNode.data] of currently | ||||||
|  |  * checking node - will be used [SortedBinaryTreeNode.leftNode] | ||||||
|  |  * | ||||||
|  |  * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or | ||||||
|  |  * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found | ||||||
|  |  */ | ||||||
|  | private suspend fun <T> SortedBinaryTreeNode<T>.upsertSubNode( | ||||||
|  |     subNode: SortedBinaryTreeNode<T>, | ||||||
|  |     skipLockers: Set<SmartRWLocker> = emptySet() | ||||||
|  | ): SortedBinaryTreeNode<T> { | ||||||
|  |     var currentlyChecking = this | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) { | ||||||
|  |                 currentlyChecking.locker.lockWrite() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val left = currentlyChecking.leftNode | ||||||
|  |             val right = currentlyChecking.rightNode | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(subNode.data, currentlyChecking.data) | ||||||
|  |             val isGreater = comparingResult > 0 | ||||||
|  |             when { | ||||||
|  |                 comparingResult == 0 -> return currentlyChecking | ||||||
|  |                 isGreater && right == null -> { | ||||||
|  |                     currentlyChecking.rightNode = subNode | ||||||
|  |                     return subNode | ||||||
|  |                 } | ||||||
|  |                 isGreater && right != null -> { | ||||||
|  |                     currentlyChecking = right | ||||||
|  |                 } | ||||||
|  |                 left == null -> { | ||||||
|  |                     currentlyChecking.leftNode = subNode | ||||||
|  |                     return subNode | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  |                     currentlyChecking = left | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.unlockWrite() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to add node") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will add subnode in tree if there are no any node with [newData] | ||||||
|  |  * | ||||||
|  |  * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, | ||||||
|  |  * will be used [SortedBinaryTreeNode.rightNode] | ||||||
|  |  * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently | ||||||
|  |  * checking node - will be returned currently checking node | ||||||
|  |  * * If [newData] is less than [SortedBinaryTreeNode.data] of currently | ||||||
|  |  * checking node - will be used [SortedBinaryTreeNode.leftNode] | ||||||
|  |  * | ||||||
|  |  * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or | ||||||
|  |  * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found | ||||||
|  |  */ | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> { | ||||||
|  |     return upsertSubNode( | ||||||
|  |         SortedBinaryTreeNode(newData, comparator) | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.findParentNode(data: T): SortedBinaryTreeNode<T>? { | ||||||
|  |     var currentParent: SortedBinaryTreeNode<T>? = null | ||||||
|  |     var currentlyChecking: SortedBinaryTreeNode<T>? = this | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking == null) { | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers) { | ||||||
|  |                 currentlyChecking.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) | ||||||
|  |             when { | ||||||
|  |                 comparingResult > 0 -> { | ||||||
|  |                     currentParent = currentlyChecking | ||||||
|  |                     currentlyChecking = currentlyChecking.rightNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 comparingResult < 0 -> { | ||||||
|  |                     currentParent = currentlyChecking | ||||||
|  |                     currentlyChecking = currentlyChecking.leftNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 else -> return currentParent | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find node") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will remove (detach) node from tree starting with [this] [SortedBinaryTreeNode] | ||||||
|  |  * | ||||||
|  |  * @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached; | ||||||
|  |  *         null otherwise | ||||||
|  |  */ | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.removeSubNode(data: T): Pair<SortedBinaryTreeNode<T>, SortedBinaryTreeNode<T>>? { | ||||||
|  |     val onFoundToRemoveCallback: suspend SortedBinaryTreeNode<T>.(left: SortedBinaryTreeNode<T>?, right: SortedBinaryTreeNode<T>?) -> Unit = { left, right -> | ||||||
|  |         left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker)) } | ||||||
|  |         right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker)) } | ||||||
|  |     } | ||||||
|  |     while (coroutineContext.job.isActive) { | ||||||
|  |         val foundParentNode = findParentNode(data) ?: return null | ||||||
|  |         foundParentNode.locker.withWriteLock { | ||||||
|  |             val left = foundParentNode.leftNode | ||||||
|  |             val right = foundParentNode.rightNode | ||||||
|  |             when { | ||||||
|  |                 left != null && left.comparator.compare(data, left.data) == 0 -> { | ||||||
|  |                     foundParentNode.leftNode = null | ||||||
|  |                     foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode) | ||||||
|  |                     return foundParentNode to left | ||||||
|  |                 } | ||||||
|  |                 right != null && right.comparator.compare(data, right.data) == 0 -> { | ||||||
|  |                     foundParentNode.rightNode = null | ||||||
|  |                     foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode) | ||||||
|  |                     return foundParentNode to right | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  |                     return@withWriteLock // data has been changed, new search required | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to remove node") | ||||||
|  | } | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.findNode(data: T): SortedBinaryTreeNode<T>? { | ||||||
|  |     var currentlyChecking: SortedBinaryTreeNode<T>? = this | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking == null) { | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers) { | ||||||
|  |                 currentlyChecking.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) | ||||||
|  |             when { | ||||||
|  |                 comparingResult > 0 -> { | ||||||
|  |                     currentlyChecking = currentlyChecking.rightNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 comparingResult < 0 -> { | ||||||
|  |                     currentlyChecking = currentlyChecking.leftNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 else -> return currentlyChecking | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find node") | ||||||
|  | } | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.contains(data: T): Boolean = findNode(data) != null | ||||||
|  |  | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedBinaryTreeNode<T>> { | ||||||
|  |     val results = mutableSetOf<SortedBinaryTreeNode<T>>() | ||||||
|  |     val leftToCheck = mutableSetOf(this) | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     val fromComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (fromInclusiveMode) { | ||||||
|  |         { it.comparator.compare(from, it.data) <= 0 } | ||||||
|  |     } else { | ||||||
|  |         { it.comparator.compare(from, it.data) < 0 } | ||||||
|  |     } | ||||||
|  |     val toComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (toInclusiveMode) { | ||||||
|  |         { it.comparator.compare(to, it.data) >= 0 } | ||||||
|  |     } else { | ||||||
|  |         { it.comparator.compare(to, it.data) > 0 } | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) { | ||||||
|  |             val currentlyChecking = leftToCheck.first() | ||||||
|  |             leftToCheck.remove(currentlyChecking) | ||||||
|  |             if (currentlyChecking in results) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             currentlyChecking.locker.acquireRead() | ||||||
|  |             lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) { | ||||||
|  |                 results.add(currentlyChecking) | ||||||
|  |                 currentlyChecking.leftNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 currentlyChecking.rightNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             when { | ||||||
|  |                 currentlyChecking.comparator.compare(to, currentlyChecking.data) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 currentlyChecking.comparator.compare(from, currentlyChecking.data) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return results.toSet() | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find nodes range") | ||||||
|  | } | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange( | ||||||
|  |     from = from, | ||||||
|  |     to = to, | ||||||
|  |     fromInclusiveMode = true, | ||||||
|  |     toInclusiveMode = true | ||||||
|  | ) | ||||||
|  | suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRangeExcluding(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange( | ||||||
|  |     from = from, | ||||||
|  |     to = to, | ||||||
|  |     fromInclusiveMode = false, | ||||||
|  |     toInclusiveMode = false | ||||||
|  | ) | ||||||
|  | suspend fun <T : Comparable<T>> SortedBinaryTreeNode<T>.findNodesInRange(range: ClosedRange<T>): Set<SortedBinaryTreeNode<T>> = findNodesInRange( | ||||||
|  |     from = range.start, | ||||||
|  |     to = range.endInclusive, | ||||||
|  | ) | ||||||
| @@ -0,0 +1,401 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.collections | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
|  | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
|  | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
|  | import kotlinx.coroutines.job | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.coroutines.coroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates simple [Comparator] which will use [compareTo] of [T] for both objects | ||||||
|  |  */ | ||||||
|  | private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) } | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | class SortedMapLikeBinaryTreeNode<K, V>( | ||||||
|  |     val key: K, | ||||||
|  |     val value: V, | ||||||
|  |     internal val comparator: Comparator<K>, | ||||||
|  | ) : Iterable<SortedMapLikeBinaryTreeNode<K, V>> { | ||||||
|  |     internal var leftNode: SortedMapLikeBinaryTreeNode<K, V>? = null | ||||||
|  |     internal var rightNode: SortedMapLikeBinaryTreeNode<K, V>? = null | ||||||
|  |     internal val locker: SmartRWLocker by lazy { | ||||||
|  |         SmartRWLocker() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getLeftNode() = locker.withReadAcquire { | ||||||
|  |         leftNode | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getRightNode() = locker.withReadAcquire { | ||||||
|  |         rightNode | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getLeftKey() = getLeftNode() ?.key | ||||||
|  |     suspend fun getLeftValue() = getLeftNode() ?.value | ||||||
|  |  | ||||||
|  |     suspend fun getRightKey() = getRightNode() ?.value | ||||||
|  |     suspend fun getRightValue() = getRightNode() ?.value | ||||||
|  |  | ||||||
|  |     override fun equals(other: Any?): Boolean { | ||||||
|  |         return other === this || (other is SortedMapLikeBinaryTreeNode<*, *> && other.key == key && other.rightNode == rightNode && other.leftNode == leftNode) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun hashCode(): Int { | ||||||
|  |         return key.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun size(): Int { | ||||||
|  |         return locker.withReadAcquire { | ||||||
|  |             1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This [Iterator] will run from less to greater values of nodes starting the | ||||||
|  |      * [dev.inmo.micro_utils.coroutines.collections.SortedMapLikeBinaryTreeNode]-receiver. Due to non-suspending | ||||||
|  |      * nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It | ||||||
|  |      * means, that tree can be changed during to iteration process | ||||||
|  |      */ | ||||||
|  |     override fun iterator(): Iterator<SortedMapLikeBinaryTreeNode<K, V>> = iterator { | ||||||
|  |         leftNode ?.let { | ||||||
|  |             it.iterator().forEach { yield(it) } | ||||||
|  |         } | ||||||
|  |         yield(this@SortedMapLikeBinaryTreeNode) | ||||||
|  |         rightNode ?.let { | ||||||
|  |             it.iterator().forEach { yield(it) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun toString(): String { | ||||||
|  |         return "$key($leftNode;$rightNode)" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         operator fun <K : Comparable<K>, V> invoke( | ||||||
|  |             key: K, | ||||||
|  |             value: V | ||||||
|  |         ) = SortedMapLikeBinaryTreeNode( | ||||||
|  |             key, | ||||||
|  |             value, | ||||||
|  |             key.createComparator() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will add subnode in tree if there are no any node with [newData] | ||||||
|  |  * | ||||||
|  |  * * If [newData] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node, | ||||||
|  |  * will be used [SortedMapLikeBinaryTreeNode.rightNode] | ||||||
|  |  * * If [newData] is equal to [SortedMapLikeBinaryTreeNode.key] of currently | ||||||
|  |  * checking node - will be returned currently checking node | ||||||
|  |  * * If [newData] is less than [SortedMapLikeBinaryTreeNode.key] of currently | ||||||
|  |  * checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode] | ||||||
|  |  * | ||||||
|  |  * This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or | ||||||
|  |  * [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [newData] will be found | ||||||
|  |  * | ||||||
|  |  * @param replaceMode Will replace only value if node already exists | ||||||
|  |  */ | ||||||
|  | private suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode( | ||||||
|  |     subNode: SortedMapLikeBinaryTreeNode<K, V>, | ||||||
|  |     skipLockers: Set<SmartRWLocker> = emptySet(), | ||||||
|  |     replaceMode: Boolean | ||||||
|  | ): SortedMapLikeBinaryTreeNode<K, V> { | ||||||
|  |     var currentlyChecking = this | ||||||
|  |     var latestParent: SortedMapLikeBinaryTreeNode<K, V>? = null | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) { | ||||||
|  |                 currentlyChecking.locker.lockWrite() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val left = currentlyChecking.leftNode | ||||||
|  |             val right = currentlyChecking.rightNode | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(subNode.key, currentlyChecking.key) | ||||||
|  |             val isGreater = comparingResult > 0 | ||||||
|  |             when { | ||||||
|  |                 comparingResult == 0 -> { | ||||||
|  |                     val resultNode = if (replaceMode) { | ||||||
|  |                         subNode | ||||||
|  |                     } else { | ||||||
|  |                         val newNode = SortedMapLikeBinaryTreeNode( | ||||||
|  |                             subNode.key, | ||||||
|  |                             subNode.value, | ||||||
|  |                             currentlyChecking.comparator, | ||||||
|  |                         ) | ||||||
|  |                         newNode.leftNode = currentlyChecking.leftNode | ||||||
|  |                         newNode.rightNode = currentlyChecking.rightNode | ||||||
|  |                         newNode | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     latestParent ?.let { | ||||||
|  |                         when { | ||||||
|  |                             it.leftNode === currentlyChecking -> it.leftNode = resultNode | ||||||
|  |                             it.rightNode === currentlyChecking -> it.rightNode = resultNode | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     return resultNode | ||||||
|  |                 } | ||||||
|  |                 isGreater && right == null -> { | ||||||
|  |                     currentlyChecking.rightNode = subNode | ||||||
|  |                     return subNode | ||||||
|  |                 } | ||||||
|  |                 isGreater && right != null -> { | ||||||
|  |                     latestParent = currentlyChecking | ||||||
|  |                     currentlyChecking = right | ||||||
|  |                 } | ||||||
|  |                 left == null -> { | ||||||
|  |                     currentlyChecking.leftNode = subNode | ||||||
|  |                     return subNode | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  |                     latestParent = currentlyChecking | ||||||
|  |                     currentlyChecking = left | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.unlockWrite() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to add node") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will add subnode in tree if there are no any node with [key] | ||||||
|  |  * | ||||||
|  |  * * If [key] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node, | ||||||
|  |  * will be used [SortedMapLikeBinaryTreeNode.rightNode] | ||||||
|  |  * * If [key] is equal to [SortedMapLikeBinaryTreeNode.key] of currently | ||||||
|  |  * checking node - will be returned currently checking node | ||||||
|  |  * * If [key] is less than [SortedMapLikeBinaryTreeNode.key] of currently | ||||||
|  |  * checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode] | ||||||
|  |  * | ||||||
|  |  * This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or | ||||||
|  |  * [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [key] will be found | ||||||
|  |  */ | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode( | ||||||
|  |     key: K, | ||||||
|  |     value: V | ||||||
|  | ): SortedMapLikeBinaryTreeNode<K, V> { | ||||||
|  |     return upsertSubNode( | ||||||
|  |         SortedMapLikeBinaryTreeNode(key, value, comparator), | ||||||
|  |         replaceMode = false | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findParentNode(data: K): SortedMapLikeBinaryTreeNode<K, V>? { | ||||||
|  |     var currentParent: SortedMapLikeBinaryTreeNode<K, V>? = null | ||||||
|  |     var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking == null) { | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers) { | ||||||
|  |                 currentlyChecking.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.key) | ||||||
|  |             when { | ||||||
|  |                 comparingResult > 0 -> { | ||||||
|  |                     currentParent = currentlyChecking | ||||||
|  |                     currentlyChecking = currentlyChecking.rightNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 comparingResult < 0 -> { | ||||||
|  |                     currentParent = currentlyChecking | ||||||
|  |                     currentlyChecking = currentlyChecking.leftNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 else -> return currentParent | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find node") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will remove (detach) node from tree starting with [this] [SortedMapLikeBinaryTreeNode] | ||||||
|  |  * | ||||||
|  |  * @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached; | ||||||
|  |  *         null otherwise | ||||||
|  |  */ | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.removeSubNode(data: K): Pair<SortedMapLikeBinaryTreeNode<K, V>, SortedMapLikeBinaryTreeNode<K, V>>? { | ||||||
|  |     val onFoundToRemoveCallback: suspend SortedMapLikeBinaryTreeNode<K, V>.(left: SortedMapLikeBinaryTreeNode<K, V>?, right: SortedMapLikeBinaryTreeNode<K, V>?) -> Unit = { left, right -> | ||||||
|  |         left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker), replaceMode = true) } | ||||||
|  |         right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker), replaceMode = true) } | ||||||
|  |     } | ||||||
|  |     while (coroutineContext.job.isActive) { | ||||||
|  |         val foundParentNode = findParentNode(data) ?: return null | ||||||
|  |         foundParentNode.locker.withWriteLock { | ||||||
|  |             val left = foundParentNode.leftNode | ||||||
|  |             val right = foundParentNode.rightNode | ||||||
|  |             when { | ||||||
|  |                 left != null && left.comparator.compare(data, left.key) == 0 -> { | ||||||
|  |                     foundParentNode.leftNode = null | ||||||
|  |                     foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode) | ||||||
|  |                     return foundParentNode to left | ||||||
|  |                 } | ||||||
|  |                 right != null && right.comparator.compare(data, right.key) == 0 -> { | ||||||
|  |                     foundParentNode.rightNode = null | ||||||
|  |                     foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode) | ||||||
|  |                     return foundParentNode to right | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  |                     return@withWriteLock // data has been changed, new search required | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to remove node") | ||||||
|  | } | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNode(key: K): SortedMapLikeBinaryTreeNode<K, V>? { | ||||||
|  |     var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive) { | ||||||
|  |             if (currentlyChecking == null) { | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |             if (currentlyChecking.locker !in lockedLockers) { | ||||||
|  |                 currentlyChecking.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             } | ||||||
|  |             val comparingResult = currentlyChecking.comparator.compare(key, currentlyChecking.key) | ||||||
|  |             when { | ||||||
|  |                 comparingResult > 0 -> { | ||||||
|  |                     currentlyChecking = currentlyChecking.rightNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 comparingResult < 0 -> { | ||||||
|  |                     currentlyChecking = currentlyChecking.leftNode | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 else -> return currentlyChecking | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find node") | ||||||
|  | } | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.contains(data: K): Boolean = findNode(data) != null | ||||||
|  |  | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedMapLikeBinaryTreeNode<K, V>> { | ||||||
|  |     val results = mutableSetOf<SortedMapLikeBinaryTreeNode<K, V>>() | ||||||
|  |     val leftToCheck = mutableSetOf(this) | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     val fromComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (fromInclusiveMode) { | ||||||
|  |         { it.comparator.compare(from, it.key) <= 0 } | ||||||
|  |     } else { | ||||||
|  |         { it.comparator.compare(from, it.key) < 0 } | ||||||
|  |     } | ||||||
|  |     val toComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (toInclusiveMode) { | ||||||
|  |         { it.comparator.compare(to, it.key) >= 0 } | ||||||
|  |     } else { | ||||||
|  |         { it.comparator.compare(to, it.key) > 0 } | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |         while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) { | ||||||
|  |             val currentlyChecking = leftToCheck.first() | ||||||
|  |             leftToCheck.remove(currentlyChecking) | ||||||
|  |             if (currentlyChecking in results) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             currentlyChecking.locker.acquireRead() | ||||||
|  |             lockedLockers.add(currentlyChecking.locker) | ||||||
|  |             if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) { | ||||||
|  |                 results.add(currentlyChecking) | ||||||
|  |                 currentlyChecking.leftNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 currentlyChecking.rightNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             when { | ||||||
|  |                 currentlyChecking.comparator.compare(to, currentlyChecking.key) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) } | ||||||
|  |                 currentlyChecking.comparator.compare(from, currentlyChecking.key) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return results.toSet() | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     error("Unable to find nodes range") | ||||||
|  | } | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.deepEquals(other: SortedMapLikeBinaryTreeNode<K, V>): Boolean { | ||||||
|  |     val leftToCheck = mutableSetOf(this) | ||||||
|  |     val othersToCheck = mutableSetOf(other) | ||||||
|  |     val lockedLockers = mutableSetOf<SmartRWLocker>() | ||||||
|  |     try { | ||||||
|  |         while (leftToCheck.isNotEmpty() && othersToCheck.isNotEmpty()) { | ||||||
|  |             val thisToCheck = leftToCheck.first() | ||||||
|  |             leftToCheck.remove(thisToCheck) | ||||||
|  |  | ||||||
|  |             val otherToCheck = othersToCheck.first() | ||||||
|  |             othersToCheck.remove(otherToCheck) | ||||||
|  |  | ||||||
|  |             if (thisToCheck.locker !in lockedLockers) { | ||||||
|  |                 thisToCheck.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(thisToCheck.locker) | ||||||
|  |             } | ||||||
|  |             if (otherToCheck.locker !in lockedLockers) { | ||||||
|  |                 otherToCheck.locker.acquireRead() | ||||||
|  |                 lockedLockers.add(otherToCheck.locker) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (thisToCheck.key != otherToCheck.key || thisToCheck.value != otherToCheck.value) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ((thisToCheck.leftNode == null).xor(otherToCheck.leftNode == null)) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             if ((thisToCheck.rightNode == null).xor(otherToCheck.rightNode == null)) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             thisToCheck.leftNode?.let { leftToCheck.add(it) } | ||||||
|  |             thisToCheck.rightNode?.let { leftToCheck.add(it) } | ||||||
|  |  | ||||||
|  |             otherToCheck.leftNode?.let { othersToCheck.add(it) } | ||||||
|  |             otherToCheck.rightNode?.let { othersToCheck.add(it) } | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         lockedLockers.forEach { | ||||||
|  |             runCatching { it.releaseRead() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return leftToCheck.isEmpty() && othersToCheck.isEmpty() | ||||||
|  | } | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange( | ||||||
|  |     from = from, | ||||||
|  |     to = to, | ||||||
|  |     fromInclusiveMode = true, | ||||||
|  |     toInclusiveMode = true | ||||||
|  | ) | ||||||
|  | suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRangeExcluding(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange( | ||||||
|  |     from = from, | ||||||
|  |     to = to, | ||||||
|  |     fromInclusiveMode = false, | ||||||
|  |     toInclusiveMode = false | ||||||
|  | ) | ||||||
|  | suspend fun <K : Comparable<K>, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(range: ClosedRange<K>): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange( | ||||||
|  |     from = range.start, | ||||||
|  |     to = range.endInclusive, | ||||||
|  | ) | ||||||
| @@ -7,7 +7,9 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T | |||||||
|     val objectToSynchronize = Object() |     val objectToSynchronize = Object() | ||||||
|     synchronized(objectToSynchronize) { |     synchronized(objectToSynchronize) { | ||||||
|         launch(start = CoroutineStart.UNDISPATCHED) { |         launch(start = CoroutineStart.UNDISPATCHED) { | ||||||
|             result = safelyWithResult(block) |             result = runCatching { | ||||||
|  |                 block() | ||||||
|  |             } | ||||||
|         }.invokeOnCompletion { |         }.invokeOnCompletion { | ||||||
|             synchronized(objectToSynchronize) { |             synchronized(objectToSynchronize) { | ||||||
|                 objectToSynchronize.notifyAll() |                 objectToSynchronize.notifyAll() | ||||||
|   | |||||||
| @@ -1,25 +1,20 @@ | |||||||
| package dev.inmo.micro_utils.coroutines | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.test.runTest | ||||||
| import kotlin.test.Test | import kotlin.test.Test | ||||||
|  |  | ||||||
| class HandleSafelyCoroutineContextTest { | class HandleSafelyCoroutineContextTest { | ||||||
|     @Test |     @Test | ||||||
|     fun testHandleSafelyCoroutineContext() { |     fun testHandleSafelyCoroutineContext() = runTest { | ||||||
|         val scope = CoroutineScope(Dispatchers.Default) |         val scope = this | ||||||
|         var contextHandlerHappen = false |         var contextHandlerHappen = false | ||||||
|         var localHandlerHappen = false |         var localHandlerHappen = false | ||||||
|         var defaultHandlerHappen = false |  | ||||||
|         defaultSafelyExceptionHandler = { |  | ||||||
|             defaultHandlerHappen = true |  | ||||||
|             throw it |  | ||||||
|         } |  | ||||||
|         val contextHandler: ExceptionHandler<Unit> = { |  | ||||||
|             contextHandlerHappen = true |  | ||||||
|         } |  | ||||||
|         val checkJob = scope.launch { |         val checkJob = scope.launch { | ||||||
|             safelyWithContextExceptionHandler(contextHandler) { |             runCatchingLogging ({ | ||||||
|                 safely( |                 contextHandlerHappen = true | ||||||
|  |             }) { | ||||||
|  |                 runCatchingLogging ( | ||||||
|                     { |                     { | ||||||
|                         localHandlerHappen = true |                         localHandlerHappen = true | ||||||
|                     } |                     } | ||||||
| @@ -29,10 +24,8 @@ class HandleSafelyCoroutineContextTest { | |||||||
|                 println(coroutineContext) |                 println(coroutineContext) | ||||||
|                 error("That must happen too:)") |                 error("That must happen too:)") | ||||||
|             } |             } | ||||||
|         } |         }.join() | ||||||
|         launchSynchronously { checkJob.join() } |  | ||||||
|         assert(contextHandlerHappen) |         assert(contextHandlerHappen) | ||||||
|         assert(localHandlerHappen) |         assert(localHandlerHappen) | ||||||
|         assert(defaultHandlerHappen) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,176 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.addSubNode | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.findNode | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.findNodesInRange | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.findParentNode | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.removeSubNode | ||||||
|  | import kotlinx.coroutines.test.runTest | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
|  | class SortedBinaryTreeNodeTests { | ||||||
|  |     @Test | ||||||
|  |     fun insertOnZeroLevelWorks() = runTest { | ||||||
|  |         val zeroNode = SortedBinaryTreeNode(0) | ||||||
|  |         zeroNode.addSubNode(1) | ||||||
|  |         zeroNode.addSubNode(-1) | ||||||
|  |  | ||||||
|  |         assertEquals(0, zeroNode.data) | ||||||
|  |         assertEquals(1, zeroNode.getRightNode() ?.data) | ||||||
|  |         assertEquals(-1, zeroNode.getLeftNode() ?.data) | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun searchOnZeroLevelWorks() = runTest { | ||||||
|  |         val zeroNode = SortedBinaryTreeNode(0) | ||||||
|  |         val oneNode = zeroNode.addSubNode(1) | ||||||
|  |         val minusOneNode = zeroNode.addSubNode(-1) | ||||||
|  |  | ||||||
|  |         val assertingNodesToSearchQuery = mapOf( | ||||||
|  |             setOf(oneNode) to (1 .. 1), | ||||||
|  |             setOf(zeroNode, oneNode) to (0 .. 1), | ||||||
|  |             setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1), | ||||||
|  |             setOf(minusOneNode, zeroNode) to (-1 .. 0), | ||||||
|  |             setOf(minusOneNode) to (-1 .. -1), | ||||||
|  |             setOf(zeroNode) to (0 .. 0), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assertingNodesToSearchQuery.forEach { | ||||||
|  |             val foundData = zeroNode.findNodesInRange(it.value) | ||||||
|  |             assertTrue(foundData.containsAll(it.key)) | ||||||
|  |             assertTrue(it.key.containsAll(foundData)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) { | ||||||
|  |         val zeroNode = SortedBinaryTreeNode(0) | ||||||
|  |         val rangeRadius = 500 | ||||||
|  |         val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>() | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             nodes[i] = zeroNode.addSubNode(i) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val foundNode = zeroNode.findNode(i) | ||||||
|  |  | ||||||
|  |             assertTrue(expectedNode === foundNode) | ||||||
|  |  | ||||||
|  |             if (expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val sourceTreeSize = zeroNode.size() | ||||||
|  |         assertTrue(sourceTreeSize == nodes.size) | ||||||
|  |         assertTrue(sourceTreeSize == (rangeRadius * 2 + 1)) | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |  | ||||||
|  |             if (parentNode == null && i == zeroNode.data && expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             assertTrue(parentNode != null, "It is expected, that parent node of ${expectedNode.data} will not be null") | ||||||
|  |  | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode.getLeftNode() === expectedNode || parentNode.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             val removeResult = zeroNode.removeSubNode(i) | ||||||
|  |             assertTrue(removeResult ?.first === parentNode) | ||||||
|  |             assertTrue(removeResult.second === expectedNode) | ||||||
|  |  | ||||||
|  |             nodes[i] = zeroNode.addSubNode(i) | ||||||
|  |             assertTrue(nodes[i] != null) | ||||||
|  |             assertTrue(nodes[i] != expectedNode) | ||||||
|  |             assertTrue(nodes[i] ?.data == i) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         assertTrue(sourceTreeSize == zeroNode.size()) | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val foundNode = zeroNode.findNode(i) | ||||||
|  |  | ||||||
|  |             assertTrue(expectedNode === foundNode) | ||||||
|  |  | ||||||
|  |             if (expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var previousData = -rangeRadius - 1 | ||||||
|  |         for (node in zeroNode) { | ||||||
|  |             assertTrue(nodes[node.data] === node) | ||||||
|  |             assertTrue(previousData == node.data - 1) | ||||||
|  |             previousData = node.data | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         assertTrue(sourceTreeSize == zeroNode.size()) | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun deepInsertOnWorks() = runTest(timeout = 240.seconds) { | ||||||
|  |         val zeroNode = SortedBinaryTreeNode(0) | ||||||
|  |         val rangeRadius = 500 | ||||||
|  |         val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>() | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             nodes[i] = zeroNode.addSubNode(i) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val foundNode = zeroNode.findNode(i) | ||||||
|  |  | ||||||
|  |             assertTrue(expectedNode === foundNode) | ||||||
|  |  | ||||||
|  |             if (expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val sourceTreeSize = zeroNode.size() | ||||||
|  |  | ||||||
|  |         var previousData = -rangeRadius - 1 | ||||||
|  |         for (node in zeroNode) { | ||||||
|  |             assertTrue(nodes[node.data] === node) | ||||||
|  |             assertTrue(previousData == node.data - 1) | ||||||
|  |             previousData = node.data | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         assertTrue(sourceTreeSize == zeroNode.size()) | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun deepInsertIteratorWorking() = runTest { | ||||||
|  |         val zeroNode = SortedBinaryTreeNode(0) | ||||||
|  |         val rangeRadius = 500 | ||||||
|  |         val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>() | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             nodes[i] = zeroNode.addSubNode(i) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var previousData = -rangeRadius - 1 | ||||||
|  |         for (node in zeroNode) { | ||||||
|  |             assertTrue(nodes[node.data] === node) | ||||||
|  |             assertTrue(previousData == node.data - 1) | ||||||
|  |             previousData = node.data | ||||||
|  |         } | ||||||
|  |         assertTrue(previousData == rangeRadius) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.collections.* | ||||||
|  | import kotlinx.coroutines.test.runTest | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
|  | class SortedMapLikeBinaryTreeNodeTests { | ||||||
|  |     @Test | ||||||
|  |     fun insertOnZeroLevelWorks() = runTest { | ||||||
|  |         val zeroNode = SortedMapLikeBinaryTreeNode(0, 0) | ||||||
|  |         zeroNode.upsertSubNode(1, 1) | ||||||
|  |         zeroNode.upsertSubNode(-1, -1) | ||||||
|  |  | ||||||
|  |         assertEquals(0, zeroNode.key) | ||||||
|  |         assertEquals(1, zeroNode.getRightNode() ?.key) | ||||||
|  |         assertEquals(-1, zeroNode.getLeftNode() ?.key) | ||||||
|  |  | ||||||
|  |         assertEquals(0, zeroNode.findNode(0) ?.value) | ||||||
|  |         assertEquals(1, zeroNode.findNode(1) ?.value) | ||||||
|  |         assertEquals(-1, zeroNode.findNode(-1) ?.value) | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun searchOnZeroLevelWorks() = runTest { | ||||||
|  |         val zeroNode = SortedMapLikeBinaryTreeNode(0, 0) | ||||||
|  |         val oneNode = zeroNode.upsertSubNode(1, 1) | ||||||
|  |         val minusOneNode = zeroNode.upsertSubNode(-1, -1) | ||||||
|  |  | ||||||
|  |         val assertingNodesToSearchQuery = mapOf( | ||||||
|  |             setOf(oneNode) to (1 .. 1), | ||||||
|  |             setOf(zeroNode, oneNode) to (0 .. 1), | ||||||
|  |             setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1), | ||||||
|  |             setOf(minusOneNode, zeroNode) to (-1 .. 0), | ||||||
|  |             setOf(minusOneNode) to (-1 .. -1), | ||||||
|  |             setOf(zeroNode) to (0 .. 0), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assertingNodesToSearchQuery.forEach { | ||||||
|  |             val foundData = zeroNode.findNodesInRange(it.value) | ||||||
|  |             assertTrue(foundData.containsAll(it.key)) | ||||||
|  |             assertTrue(it.key.containsAll(foundData)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) { | ||||||
|  |         var zeroNode = SortedMapLikeBinaryTreeNode(0, 0) | ||||||
|  |         val rangeRadius = 500 | ||||||
|  |         val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>() | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             nodes[i] = zeroNode.upsertSubNode(i, i) | ||||||
|  |             if (i == zeroNode.key) { | ||||||
|  |                 zeroNode = nodes.getValue(i) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val foundNode = zeroNode.findNode(i) | ||||||
|  |  | ||||||
|  |             assertEquals(expectedNode, foundNode) | ||||||
|  |  | ||||||
|  |             if (expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}" | ||||||
|  |             ) | ||||||
|  |             assertTrue( | ||||||
|  |                 foundNode != null && expectedNode.deepEquals(foundNode) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             zeroNode.upsertSubNode(i, -i) | ||||||
|  |             val foundModifiedNode = zeroNode.findNode(i) | ||||||
|  |             assertEquals(foundNode ?.value, foundModifiedNode ?.value ?.times(-1)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun deepInsertOnWorks() = runTest(timeout = 240.seconds) { | ||||||
|  |         val zeroNode = SortedMapLikeBinaryTreeNode(0, 0) | ||||||
|  |         val rangeRadius = 500 | ||||||
|  |         val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>() | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             if (zeroNode.key != i) { | ||||||
|  |                 nodes[i] = zeroNode.upsertSubNode(i, i) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         nodes[zeroNode.key] = zeroNode | ||||||
|  |  | ||||||
|  |         for (i in -rangeRadius .. rangeRadius) { | ||||||
|  |             val expectedNode = nodes.getValue(i) | ||||||
|  |             val foundNode = zeroNode.findNode(i) | ||||||
|  |  | ||||||
|  |             assertTrue(expectedNode === foundNode) | ||||||
|  |  | ||||||
|  |             if (expectedNode === zeroNode) continue | ||||||
|  |  | ||||||
|  |             val parentNode = zeroNode.findParentNode(i) | ||||||
|  |             assertTrue( | ||||||
|  |                 parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, | ||||||
|  |                 "It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val sourceTreeSize = zeroNode.size() | ||||||
|  |  | ||||||
|  |         var previousData = -rangeRadius - 1 | ||||||
|  |         for (node in zeroNode) { | ||||||
|  |             assertTrue(nodes[node.key] === node) | ||||||
|  |             assertTrue(previousData == node.key - 1) | ||||||
|  |             previousData = node.key | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         assertTrue(sourceTreeSize == zeroNode.size()) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -128,8 +128,10 @@ open class DefaultStatesMachine <T: State>( | |||||||
|      */ |      */ | ||||||
|     override fun start(scope: CoroutineScope): Job { |     override fun start(scope: CoroutineScope): Job { | ||||||
|         val supervisorScope = scope.LinkedSupervisorScope() |         val supervisorScope = scope.LinkedSupervisorScope() | ||||||
|         supervisorScope.launchSafelyWithoutExceptions { |         supervisorScope.launchLoggingDropExceptions { | ||||||
|             (statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(supervisorScope) { |             (statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions( | ||||||
|  |                 supervisorScope | ||||||
|  |             ) { | ||||||
|                 supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) } |                 supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) } | ||||||
|             } |             } | ||||||
|             statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) { |             statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) { | ||||||
| @@ -140,7 +142,7 @@ open class DefaultStatesMachine <T: State>( | |||||||
|                     statesJobsMutex.withLock { |                     statesJobsMutex.withLock { | ||||||
|                         val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState } |                         val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState } | ||||||
|                         if (stateInMap === removedState) { |                         if (stateInMap === removedState) { | ||||||
|                             statesJobs[stateInMap] ?.cancel() |                             statesJobs[stateInMap]?.cancel() | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -15,5 +15,5 @@ crypto_js_version=4.1.1 | |||||||
| # Project data | # Project data | ||||||
|  |  | ||||||
| group=dev.inmo | group=dev.inmo | ||||||
| version=0.24.4 | version=0.25.2 | ||||||
| android_code_version=283 | android_code_version=292 | ||||||
|   | |||||||
| @@ -1,43 +1,42 @@ | |||||||
| [versions] | [versions] | ||||||
|  |  | ||||||
| kt = "2.1.0" | kt = "2.1.10" | ||||||
| kt-serialization = "1.8.0" | kt-serialization = "1.8.0" | ||||||
| kt-coroutines = "1.10.1" | kt-coroutines = "1.10.1" | ||||||
|  |  | ||||||
| kslog = "1.4.0" | kslog = "1.4.1" | ||||||
|  |  | ||||||
| jb-compose = "1.7.3" | jb-compose = "1.7.3" | ||||||
| jb-exposed = "0.58.0" | jb-exposed = "0.60.0" | ||||||
| jb-dokka = "2.0.0" | jb-dokka = "2.0.0" | ||||||
|  |  | ||||||
| sqlite = "3.47.2.0" | sqlite = "3.49.1.0" | ||||||
|  |  | ||||||
| korlibs = "5.4.0" | korlibs = "5.4.0" | ||||||
| uuid = "0.8.4" | uuid = "0.8.4" | ||||||
|  |  | ||||||
| ktor = "3.0.3" | ktor = "3.1.1" | ||||||
|  |  | ||||||
| gh-release = "2.5.2" | gh-release = "2.5.2" | ||||||
|  |  | ||||||
| koin = "4.0.1" | koin = "4.0.2" | ||||||
|  |  | ||||||
| okio = "3.10.2" | okio = "3.10.2" | ||||||
|  |  | ||||||
| ksp = "2.1.0-1.0.29" | ksp = "2.1.10-1.0.31" | ||||||
| kotlin-poet = "1.18.1" | kotlin-poet = "1.18.1" | ||||||
|  |  | ||||||
| versions = "0.51.0" | versions = "0.51.0" | ||||||
|  |  | ||||||
| android-gradle = "8.2.2" | android-gradle = "8.7.+" | ||||||
| dexcount = "4.0.0" | dexcount = "4.0.0" | ||||||
|  |  | ||||||
| android-coreKtx = "1.15.0" | android-coreKtx = "1.15.0" | ||||||
| android-recyclerView = "1.3.2" | android-recyclerView = "1.4.0" | ||||||
| android-appCompat = "1.7.0" | android-appCompat = "1.7.0" | ||||||
| android-fragment = "1.8.5" | android-fragment = "1.8.6" | ||||||
| android-espresso = "3.6.1" | android-espresso = "3.6.1" | ||||||
| android-test = "1.2.1" | android-test = "1.2.1" | ||||||
| android-compose-material3 = "1.3.0" |  | ||||||
|  |  | ||||||
| android-props-minSdk = "21" | android-props-minSdk = "21" | ||||||
| android-props-compileSdk = "35" | android-props-compileSdk = "35" | ||||||
|   | |||||||
| @@ -15,14 +15,14 @@ kotlin { | |||||||
|         browser { |         browser { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         nodejs { |         nodejs { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -15,14 +15,14 @@ kotlin { | |||||||
|         browser { |         browser { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         nodejs { |         nodejs { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -15,14 +15,14 @@ kotlin { | |||||||
|         browser { |         browser { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         nodejs { |         nodejs { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -15,14 +15,14 @@ kotlin { | |||||||
|         browser { |         browser { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         nodejs { |         nodejs { | ||||||
|             testTask { |             testTask { | ||||||
|                 useMocha { |                 useMocha { | ||||||
|                     timeout = "60000" |                     timeout = "240000" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,3 +1,7 @@ | |||||||
|  | if (ext.getProperties()["do_publish"] == false) { | ||||||
|  |     return | ||||||
|  | } | ||||||
|  |  | ||||||
| apply plugin: 'maven-publish' | apply plugin: 'maven-publish' | ||||||
|  |  | ||||||
| task javadocsJar(type: Jar) { | task javadocsJar(type: Jar) { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| distributionBase=GRADLE_USER_HOME | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|   | |||||||
| @@ -17,11 +17,13 @@ kotlin { | |||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.reflect |                 api libs.kt.reflect | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.reflect |                 api libs.kt.reflect | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								koin/src/jvmMain/kotlin/FactorySuspend.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								koin/src/jvmMain/kotlin/FactorySuspend.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package dev.inmo.micro_utils.koin | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.doSynchronously | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import org.koin.core.module.Module | ||||||
|  | import org.koin.core.parameter.ParametersHolder | ||||||
|  | import org.koin.core.qualifier.Qualifier | ||||||
|  | import org.koin.core.qualifier.StringQualifier | ||||||
|  | import org.koin.core.scope.Scope | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | inline fun <reified T : Any> Module.factorySuspend( | ||||||
|  |     qualifier: Qualifier? = null, | ||||||
|  |     coroutineScope: CoroutineScope? = null, | ||||||
|  |     noinline definition: suspend Scope.(ParametersHolder) -> T | ||||||
|  | ) = factory( | ||||||
|  |     qualifier, | ||||||
|  |     if (coroutineScope == null) { | ||||||
|  |         { | ||||||
|  |             doSynchronously { | ||||||
|  |                 definition(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         { | ||||||
|  |             coroutineScope.doSynchronously { | ||||||
|  |                 definition(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								koin/src/jvmMain/kotlin/SingleSuspend.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								koin/src/jvmMain/kotlin/SingleSuspend.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package dev.inmo.micro_utils.koin | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.doSynchronously | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import org.koin.core.module.Module | ||||||
|  | import org.koin.core.parameter.ParametersHolder | ||||||
|  | import org.koin.core.qualifier.StringQualifier | ||||||
|  | import org.koin.core.scope.Scope | ||||||
|  |  | ||||||
|  | inline fun <reified T : Any> Module.singleSuspend( | ||||||
|  |     qualifier: StringQualifier, | ||||||
|  |     createdAtStart: Boolean = false, | ||||||
|  |     coroutineScope: CoroutineScope? = null, | ||||||
|  |     noinline definition: suspend Scope.(ParametersHolder) -> T | ||||||
|  | ) = single( | ||||||
|  |     qualifier, | ||||||
|  |     createdAtStart, | ||||||
|  |     if (coroutineScope == null) { | ||||||
|  |         { | ||||||
|  |             doSynchronously { | ||||||
|  |                 definition(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         { | ||||||
|  |             coroutineScope.doSynchronously { | ||||||
|  |                 definition(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
| @@ -5,6 +5,8 @@ plugins { | |||||||
|     id "com.google.devtools.ksp" |     id "com.google.devtools.ksp" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ext.do_publish = false | ||||||
|  |  | ||||||
| apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,28 +1,38 @@ | |||||||
| package dev.inmo.micro_ksp.generator | package dev.inmo.micro_ksp.generator | ||||||
|  |  | ||||||
| import com.google.devtools.ksp.symbol.KSClassDeclaration | import com.google.devtools.ksp.symbol.KSClassDeclaration | ||||||
|  | import com.google.devtools.ksp.symbol.KSDeclaration | ||||||
| import com.google.devtools.ksp.symbol.KSFile | import com.google.devtools.ksp.symbol.KSFile | ||||||
|  | import com.google.devtools.ksp.symbol.KSFunctionDeclaration | ||||||
| import com.squareup.kotlinpoet.FileSpec | import com.squareup.kotlinpoet.FileSpec | ||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
| fun KSClassDeclaration.writeFile( | fun KSDeclaration.writeFile( | ||||||
|     prefix: String = "", |     prefix: String = "", | ||||||
|     suffix: String = "", |     suffix: String = "", | ||||||
|     relatedPath: String = "", |     relatedPath: String = "", | ||||||
|     force: Boolean = false, |     force: Boolean = false, | ||||||
|  |     forceUppercase: Boolean = true, | ||||||
|     fileSpecBuilder: () -> FileSpec |     fileSpecBuilder: () -> FileSpec | ||||||
| ) { | ) { | ||||||
|     val containingFile = containingFile!! |     val containingFile = containingFile!! | ||||||
|  |     val simpleName = if (forceUppercase) { | ||||||
|  |         val rawSimpleName = simpleName.asString() | ||||||
|  |         rawSimpleName.replaceFirst(rawSimpleName.first().toString(), rawSimpleName.first().uppercase()) | ||||||
|  |     } else { | ||||||
|  |         simpleName.asString() | ||||||
|  |     } | ||||||
|     File( |     File( | ||||||
|         File( |         File( | ||||||
|             File(containingFile.filePath).parent, |             File(containingFile.filePath).parent, | ||||||
|             relatedPath |             relatedPath | ||||||
|         ), |         ), | ||||||
|         "$prefix${simpleName.asString()}$suffix.kt" |         "$prefix${simpleName}$suffix.kt" | ||||||
|     ).takeIf { force || !it.exists() } ?.apply { |     ).takeIf { force || !it.exists() } ?.apply { | ||||||
|         parentFile.mkdirs() |         parentFile.mkdirs() | ||||||
|  |         val fileSpec = fileSpecBuilder() | ||||||
|         writer().use { writer -> |         writer().use { writer -> | ||||||
|             fileSpecBuilder().writeTo(writer) |             fileSpec.writeTo(writer) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -42,8 +52,9 @@ fun KSFile.writeFile( | |||||||
|         "$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt" |         "$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt" | ||||||
|     ).takeIf { force || !it.exists() } ?.apply { |     ).takeIf { force || !it.exists() } ?.apply { | ||||||
|         parentFile.mkdirs() |         parentFile.mkdirs() | ||||||
|  |         val fileSpec = fileSpecBuilder() | ||||||
|         writer().use { writer -> |         writer().use { writer -> | ||||||
|             fileSpecBuilder().writeTo(writer) |             fileSpec.writeTo(writer) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										25
									
								
								ksp/generator/src/main/kotlin/KClassWorkarounds.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ksp/generator/src/main/kotlin/KClassWorkarounds.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_ksp.generator | ||||||
|  |  | ||||||
|  | import com.google.devtools.ksp.KSTypeNotPresentException | ||||||
|  | import com.google.devtools.ksp.KSTypesNotPresentException | ||||||
|  | import com.google.devtools.ksp.KspExperimental | ||||||
|  | import com.google.devtools.ksp.symbol.KSType | ||||||
|  | import com.squareup.kotlinpoet.asClassName | ||||||
|  | import com.squareup.kotlinpoet.ksp.toClassName | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | @OptIn(KspExperimental::class) | ||||||
|  | inline fun convertToClassName(getter: () -> KClass<*>) = try { | ||||||
|  |     getter().asClassName() | ||||||
|  | } catch (e: KSTypeNotPresentException) { | ||||||
|  |     e.ksType.toClassName() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(KspExperimental::class) | ||||||
|  | inline fun convertToClassNames(getter: () -> List<KClass<*>>) = try { | ||||||
|  |     getter().map { it.asClassName() } | ||||||
|  | } catch (e: KSTypesNotPresentException) { | ||||||
|  |     e.ksTypes.map { | ||||||
|  |         it.toClassName() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -5,6 +5,8 @@ plugins { | |||||||
|     id "com.google.devtools.ksp" |     id "com.google.devtools.ksp" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ext.do_publish = false | ||||||
|  |  | ||||||
| apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								ksp/variations/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ksp/variations/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
							
								
								
									
										22
									
								
								ksp/variations/generator/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ksp/variations/generator/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.jvm" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$publish_jvm" | ||||||
|  |  | ||||||
|  | repositories { | ||||||
|  |     mavenCentral() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     implementation libs.kt.stdlib | ||||||
|  |     api project(":micro_utils.ksp.generator") | ||||||
|  |     api project(":micro_utils.ksp.variations") | ||||||
|  |     api libs.kotlin.poet | ||||||
|  |     api libs.ksp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | java { | ||||||
|  |     sourceCompatibility = JavaVersion.VERSION_17 | ||||||
|  |     targetCompatibility = JavaVersion.VERSION_17 | ||||||
|  | } | ||||||
							
								
								
									
										243
									
								
								ksp/variations/generator/src/main/kotlin/Processor.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								ksp/variations/generator/src/main/kotlin/Processor.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | |||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator | ||||||
|  |  | ||||||
|  | import com.google.devtools.ksp.KSTypeNotPresentException | ||||||
|  | import com.google.devtools.ksp.KspExperimental | ||||||
|  | import com.google.devtools.ksp.getAnnotationsByType | ||||||
|  | import com.google.devtools.ksp.processing.CodeGenerator | ||||||
|  | import com.google.devtools.ksp.processing.Resolver | ||||||
|  | import com.google.devtools.ksp.processing.SymbolProcessor | ||||||
|  | import com.google.devtools.ksp.symbol.* | ||||||
|  | import com.squareup.kotlinpoet.* | ||||||
|  | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||||||
|  | import com.squareup.kotlinpoet.ksp.toAnnotationSpec | ||||||
|  | import com.squareup.kotlinpoet.ksp.toClassName | ||||||
|  | import com.squareup.kotlinpoet.ksp.toKModifier | ||||||
|  | import com.squareup.kotlinpoet.ksp.toTypeName | ||||||
|  | import dev.inmo.micro_ksp.generator.convertToClassName | ||||||
|  | import dev.inmo.micro_ksp.generator.convertToClassNames | ||||||
|  | import dev.inmo.micro_ksp.generator.findSubClasses | ||||||
|  | import dev.inmo.micro_ksp.generator.writeFile | ||||||
|  | import dev.inmo.micro_utils.ksp.variations.GenerateVariations | ||||||
|  | import dev.inmo.micro_utils.ksp.variations.GenerationVariant | ||||||
|  | import kotlin.math.pow | ||||||
|  |  | ||||||
|  | class Processor( | ||||||
|  |     private val codeGenerator: CodeGenerator | ||||||
|  | ) : SymbolProcessor { | ||||||
|  |     private fun KSClassDeclaration.findSealedConnection(potentialSealedParent: KSClassDeclaration): Boolean { | ||||||
|  |         val targetClassname = potentialSealedParent.qualifiedName ?.asString() | ||||||
|  |         return superTypes.any { | ||||||
|  |             val itAsDeclaration = it.resolve().declaration as? KSClassDeclaration ?: return@any false | ||||||
|  |             targetClassname == (itAsDeclaration.qualifiedName ?.asString()) || (itAsDeclaration.getSealedSubclasses().any() && itAsDeclaration.findSealedConnection(potentialSealedParent)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun KSClassDeclaration.resolveSubclasses( | ||||||
|  |         searchIn: Sequence<KSAnnotated>, | ||||||
|  |         allowNonSealed: Boolean | ||||||
|  |     ): Sequence<KSClassDeclaration> { | ||||||
|  |         return findSubClasses(searchIn).let { | ||||||
|  |             if (allowNonSealed) { | ||||||
|  |                 it | ||||||
|  |             } else { | ||||||
|  |                 it.filter { | ||||||
|  |                     it.findSealedConnection(this) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @OptIn(KspExperimental::class) | ||||||
|  |     private fun FileSpec.Builder.generateVariations( | ||||||
|  |         ksFunctionDeclaration: KSFunctionDeclaration, | ||||||
|  |         resolver: Resolver | ||||||
|  |     ) { | ||||||
|  |         val annotation = ksFunctionDeclaration.getAnnotationsByType(GenerateVariations::class).first() | ||||||
|  |         val variations: List<Pair<List<GenerationVariant>, KSValueParameter>> = ksFunctionDeclaration.parameters.mapNotNull { | ||||||
|  |             val variationAnnotations = it.getAnnotationsByType(GenerationVariant::class).toList() | ||||||
|  |             variationAnnotations to it | ||||||
|  |         } | ||||||
|  |         val accumulatedGenerations = mutableSetOf<Pair<FunSpec, Map<String, String>>>() | ||||||
|  |         val baseFunctionParameters = ksFunctionDeclaration.parameters.mapNotNull { | ||||||
|  |             ParameterSpec | ||||||
|  |                 .builder( | ||||||
|  |                     it.name ?.asString() ?: return@mapNotNull null, | ||||||
|  |                     it.type.toTypeName(), | ||||||
|  |                 ) | ||||||
|  |                 .apply { | ||||||
|  |                     if (it.isCrossInline) { | ||||||
|  |                         addModifiers(KModifier.CROSSINLINE) | ||||||
|  |                     } | ||||||
|  |                     if (it.isVal) { | ||||||
|  |                         addModifiers(KModifier.VALUE) | ||||||
|  |                     } | ||||||
|  |                     if (it.isNoInline) { | ||||||
|  |                         addModifiers(KModifier.NOINLINE) | ||||||
|  |                     } | ||||||
|  |                     if (it.isVararg) { | ||||||
|  |                         addModifiers(KModifier.VARARG) | ||||||
|  |                     } | ||||||
|  |                     it.annotations.forEach { | ||||||
|  |                         if (it.shortName.asString() == GenerationVariant::class.simpleName) return@forEach | ||||||
|  |  | ||||||
|  |                         addAnnotation(it.toAnnotationSpec(omitDefaultValues = false)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .build() to it.hasDefault | ||||||
|  |         } | ||||||
|  |         val baseFunctionFunSpecs = mutableListOf<Pair<FunSpec, Map<String, String>>>() | ||||||
|  |         let { | ||||||
|  |             var defaultParametersIndicator = 0u | ||||||
|  |             val maxIndicator = baseFunctionParameters.filter { it.second }.foldIndexed(0u) { index, acc, _ -> | ||||||
|  |                 2.0.pow(index).toUInt() + acc | ||||||
|  |             } | ||||||
|  |             while (defaultParametersIndicator <= maxIndicator) { | ||||||
|  |                 var currentDefaultParameterIndex = 0u | ||||||
|  |                 val baseFunctionDefaults = mutableMapOf<String, String>() | ||||||
|  |                 val funSpec = FunSpec.builder(ksFunctionDeclaration.simpleName.asString()).apply { | ||||||
|  |                     modifiers.addAll(ksFunctionDeclaration.modifiers.mapNotNull { it.toKModifier() }) | ||||||
|  |                     ksFunctionDeclaration.annotations.forEach { | ||||||
|  |                         if (it.shortName.asString() == GenerateVariations::class.simpleName) return@forEach | ||||||
|  |  | ||||||
|  |                         addAnnotation(it.toAnnotationSpec(omitDefaultValues = false)) | ||||||
|  |                     } | ||||||
|  |                     ksFunctionDeclaration.extensionReceiver ?.let { | ||||||
|  |                         receiver(it.toTypeName()) | ||||||
|  |                     } | ||||||
|  |                     ksFunctionDeclaration.returnType ?.let { | ||||||
|  |                         returns(it.toTypeName()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 baseFunctionParameters.forEach { (parameter, hasDefault) -> | ||||||
|  |                     if (hasDefault) { | ||||||
|  |                         val shouldBeIncluded = (2.0.pow(currentDefaultParameterIndex.toInt()).toUInt()).and(defaultParametersIndicator) > 0u | ||||||
|  |                         currentDefaultParameterIndex++ | ||||||
|  |  | ||||||
|  |                         if (!shouldBeIncluded) { | ||||||
|  |                             return@forEach | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     funSpec.addParameter(parameter) | ||||||
|  |                     val name = parameter.name | ||||||
|  |                     val defaultValueString = if (parameter.modifiers.contains(KModifier.VARARG)) { | ||||||
|  |                         "*$name" | ||||||
|  |                     } else { | ||||||
|  |                         "$name" | ||||||
|  |                     } | ||||||
|  |                     baseFunctionDefaults[parameter.name] = defaultValueString | ||||||
|  |                 } | ||||||
|  |                 baseFunctionFunSpecs.add( | ||||||
|  |                     funSpec.build() to baseFunctionDefaults.toMap() | ||||||
|  |                 ) | ||||||
|  |                 defaultParametersIndicator++ | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         variations.forEach { (variations, parameter) -> | ||||||
|  |             (baseFunctionFunSpecs + accumulatedGenerations).forEach { (accumulatedGeneration, baseDefaults) -> | ||||||
|  |                 if ((parameter.name ?.asString() ?: "this") !in baseDefaults.keys) { | ||||||
|  |                     return@forEach | ||||||
|  |                 } | ||||||
|  |                 variations.forEach { variation -> | ||||||
|  |                     val defaults = mutableMapOf<String, String>() | ||||||
|  |                     accumulatedGenerations.add( | ||||||
|  |                         FunSpec.builder(accumulatedGeneration.name).apply { | ||||||
|  |                             modifiers.addAll(accumulatedGeneration.modifiers) | ||||||
|  |                             accumulatedGeneration.annotations.forEach { | ||||||
|  |                                 addAnnotation(it) | ||||||
|  |                             } | ||||||
|  |                             accumulatedGeneration.receiverType ?.let { | ||||||
|  |                                 receiver(it) | ||||||
|  |                             } | ||||||
|  |                             returns(accumulatedGeneration.returnType) | ||||||
|  |                             accumulatedGeneration.parameters.forEach { | ||||||
|  |                                 val actualName = if (variation.argName.isEmpty()) it.name else variation.argName | ||||||
|  |                                 parameters.add( | ||||||
|  |                                     (if (it.name == (parameter.name ?.asString() ?: "this")) { | ||||||
|  |                                         val type = convertToClassName { variation.type } | ||||||
|  |                                         val genericTypes = convertToClassNames { variation.genericTypes.toList()  } | ||||||
|  |                                         ParameterSpec | ||||||
|  |                                             .builder( | ||||||
|  |                                                 actualName, | ||||||
|  |                                                 if (genericTypes.isEmpty()) { | ||||||
|  |                                                     type | ||||||
|  |                                                 } else { | ||||||
|  |                                                     type.parameterizedBy( | ||||||
|  |                                                         *genericTypes.toTypedArray() | ||||||
|  |                                                     ) | ||||||
|  |                                                 } | ||||||
|  |                                             ) | ||||||
|  |                                             .apply { | ||||||
|  |                                                 addModifiers(it.modifiers) | ||||||
|  |                                                 val defaultValueString = """ | ||||||
|  |                                                     with(${actualName}) {${ | ||||||
|  |                                                             if (it.modifiers.contains(KModifier.VARARG)) { | ||||||
|  |                                                                 "map { it.${variation.conversion} }.toTypedArray()" | ||||||
|  |                                                             } else { | ||||||
|  |                                                                 "${variation.conversion}" | ||||||
|  |                                                             } | ||||||
|  |                                                     }} | ||||||
|  |                                                 """.trimIndent() | ||||||
|  |                                                 defaults[it.name] = defaultValueString | ||||||
|  |                                             } | ||||||
|  |                                     } else { | ||||||
|  |                                         it.toBuilder().apply { | ||||||
|  |                                             defaults[it.name] = it.name | ||||||
|  |                                         } | ||||||
|  |                                     }) | ||||||
|  |                                         .apply { | ||||||
|  |                                             it.annotations.forEach { | ||||||
|  |                                                 addAnnotation(it) | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                         .build() | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                             val parameters = accumulatedGeneration.parameters.joinToString(", ") { | ||||||
|  |                                 val itName = it.name | ||||||
|  |                                 """ | ||||||
|  |                                     $itName = ${defaults[itName] ?: baseDefaults[itName] ?: itName} | ||||||
|  |                                 """.trimIndent() | ||||||
|  |                             } | ||||||
|  |                             addCode( | ||||||
|  |                                 """ | ||||||
|  |                                     return ${ksFunctionDeclaration.simpleName.asString()}( | ||||||
|  |                                         $parameters | ||||||
|  |                                     ) | ||||||
|  |                                 """.trimIndent() | ||||||
|  |                             ) | ||||||
|  |                         }.build() to defaults.toMap() | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         accumulatedGenerations.forEach { | ||||||
|  |             addFunction(it.first) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @OptIn(KspExperimental::class) | ||||||
|  |     override fun process(resolver: Resolver): List<KSAnnotated> { | ||||||
|  |         (resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach { | ||||||
|  |             val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf { | ||||||
|  |                 it.isNotEmpty() | ||||||
|  |             } ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "") | ||||||
|  |             it.writeFile(prefix = prefix, suffix = "GeneratedVariation") { | ||||||
|  |                 FileSpec.builder( | ||||||
|  |                     it.packageName.asString(), | ||||||
|  |                     "${it.simpleName.getShortName().let { it.replaceFirst(it.first().toString(), it.first().uppercase()) }}GeneratedVariation" | ||||||
|  |                 ).apply { | ||||||
|  |                     addFileComment( | ||||||
|  |                         """ | ||||||
|  |                         THIS CODE HAVE BEEN GENERATED AUTOMATICALLY | ||||||
|  |                         TO REGENERATE IT JUST DELETE FILE | ||||||
|  |                         ORIGINAL FILE: ${it.containingFile ?.fileName} | ||||||
|  |                         """.trimIndent() | ||||||
|  |                     ) | ||||||
|  |                     generateVariations(it, resolver) | ||||||
|  |                 }.build() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return emptyList() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								ksp/variations/generator/src/main/kotlin/Provider.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ksp/variations/generator/src/main/kotlin/Provider.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator | ||||||
|  |  | ||||||
|  | import com.google.devtools.ksp.processing.SymbolProcessor | ||||||
|  | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment | ||||||
|  | import com.google.devtools.ksp.processing.SymbolProcessorProvider | ||||||
|  |  | ||||||
|  | class Provider : SymbolProcessorProvider { | ||||||
|  |     override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = Processor( | ||||||
|  |         environment.codeGenerator | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | dev.inmo.micro_utils.ksp.variations.generator.Provider | ||||||
							
								
								
									
										30
									
								
								ksp/variations/generator/test/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								ksp/variations/generator/test/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     id "com.google.devtools.ksp" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ext.do_publish = false | ||||||
|  |  | ||||||
|  | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation libs.kt.stdlib | ||||||
|  |                 api project(":micro_utils.ksp.variations") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     add("kspCommonMainMetadata", project(":micro_utils.ksp.variations.generator")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ksp { | ||||||
|  | } | ||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | // THIS CODE HAVE BEEN GENERATED AUTOMATICALLY | ||||||
|  | // TO REGENERATE IT JUST DELETE FILE | ||||||
|  | // ORIGINAL FILE: SampleFun.kt | ||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator.test | ||||||
|  |  | ||||||
|  | import kotlin.Boolean | ||||||
|  | import kotlin.Int | ||||||
|  | import kotlin.String | ||||||
|  | import kotlin.Unit | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg12: Int): Unit = sample2( | ||||||
|  |     arg1 = with(arg12) {toString()} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg12: Int, arg2: Int): Unit = sample2( | ||||||
|  |     arg1 = with(arg12) {toString()}, arg2 = arg2 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg12: Int, arg3: Boolean): Unit = sample2( | ||||||
|  |     arg1 = with(arg12) {toString()}, arg3 = arg3 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2( | ||||||
|  |   arg12: Int, | ||||||
|  |   arg2: Int, | ||||||
|  |   arg3: Boolean, | ||||||
|  | ): Unit = sample2( | ||||||
|  |     arg1 = with(arg12) {toString()}, arg2 = arg2, arg3 = arg3 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg22: String): Unit = sample2( | ||||||
|  |     arg2 = with(arg22) {toInt()} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg1: String, arg22: String): Unit = sample2( | ||||||
|  |     arg1 = arg1, arg2 = with(arg22) {toInt()} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg22: String, arg3: Boolean): Unit = sample2( | ||||||
|  |     arg2 = with(arg22) {toInt()}, arg3 = arg3 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2( | ||||||
|  |   arg1: String, | ||||||
|  |   arg22: String, | ||||||
|  |   arg3: Boolean, | ||||||
|  | ): Unit = sample2( | ||||||
|  |     arg1 = arg1, arg2 = with(arg22) {toInt()}, arg3 = arg3 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2(arg12: Int, arg22: String): Unit = sample2( | ||||||
|  |     arg12 = arg12, arg2 = with(arg22) {toInt()} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public suspend fun SimpleType.sample2( | ||||||
|  |   arg12: Int, | ||||||
|  |   arg22: String, | ||||||
|  |   arg3: Boolean, | ||||||
|  | ): Unit = sample2( | ||||||
|  |     arg12 = arg12, arg2 = with(arg22) {toInt()}, arg3 = arg3 | ||||||
|  | ) | ||||||
| @@ -0,0 +1,52 @@ | |||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator.test | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.ksp.variations.GenerateVariations | ||||||
|  | import dev.inmo.micro_utils.ksp.variations.GenerationVariant | ||||||
|  |  | ||||||
|  | data class SimpleType( | ||||||
|  |     val value: String | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | data class GenericType<T>( | ||||||
|  |     val value: T | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @GenerateVariations | ||||||
|  | fun sample( | ||||||
|  |     @GenerationVariant( | ||||||
|  |         SimpleType::class, | ||||||
|  |         "value", | ||||||
|  |     ) | ||||||
|  |     @GenerationVariant( | ||||||
|  |         GenericType::class, | ||||||
|  |         "value.toString()", | ||||||
|  |         genericTypes = arrayOf(Int::class) | ||||||
|  |     ) | ||||||
|  |     example: String = "12" | ||||||
|  | ) = println(example) | ||||||
|  |  | ||||||
|  | @GenerateVariations | ||||||
|  | fun sampleVararg( | ||||||
|  |     @GenerationVariant( | ||||||
|  |         SimpleType::class, | ||||||
|  |         "value", | ||||||
|  |     ) | ||||||
|  |     vararg example: String = arrayOf("12") | ||||||
|  | ) = println(example.joinToString()) | ||||||
|  |  | ||||||
|  | @GenerateVariations | ||||||
|  | suspend fun SimpleType.sample2( | ||||||
|  |     @GenerationVariant( | ||||||
|  |         Int::class, | ||||||
|  |         "toString()", | ||||||
|  |         "arg12", | ||||||
|  |     ) | ||||||
|  |     arg1: String = "1", | ||||||
|  |     @GenerationVariant( | ||||||
|  |         String::class, | ||||||
|  |         "toInt()", | ||||||
|  |         "arg22", | ||||||
|  |     ) | ||||||
|  |     arg2: Int = 2, | ||||||
|  |     arg3: Boolean = false | ||||||
|  | ) = println(arg1) | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | // THIS CODE HAVE BEEN GENERATED AUTOMATICALLY | ||||||
|  | // TO REGENERATE IT JUST DELETE FILE | ||||||
|  | // ORIGINAL FILE: SampleFun.kt | ||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator.test | ||||||
|  |  | ||||||
|  | import kotlin.Int | ||||||
|  | import kotlin.Unit | ||||||
|  |  | ||||||
|  | public fun sample(example: SimpleType): Unit = sample( | ||||||
|  |     example = with(example) {value} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | public fun sample(example: GenericType<Int>): Unit = sample( | ||||||
|  |     example = with(example) {value.toString()} | ||||||
|  | ) | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | // THIS CODE HAVE BEEN GENERATED AUTOMATICALLY | ||||||
|  | // TO REGENERATE IT JUST DELETE FILE | ||||||
|  | // ORIGINAL FILE: SampleFun.kt | ||||||
|  | package dev.inmo.micro_utils.ksp.variations.generator.test | ||||||
|  |  | ||||||
|  | import kotlin.Unit | ||||||
|  |  | ||||||
|  | public fun sampleVararg(vararg example: SimpleType): Unit = sampleVararg( | ||||||
|  |     example = with(example) {map { it.value }.toTypedArray()} | ||||||
|  | ) | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package dev.inmo.micro_utils.ksp.variations | ||||||
|  |  | ||||||
|  | @Retention(AnnotationRetention.BINARY) | ||||||
|  | @Target(AnnotationTarget.FUNCTION) | ||||||
|  | annotation class GenerateVariations( | ||||||
|  |     val prefix: String = "" | ||||||
|  | ) | ||||||
							
								
								
									
										18
									
								
								ksp/variations/src/commonMain/kotlin/GenerationVariant.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ksp/variations/src/commonMain/kotlin/GenerationVariant.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | package dev.inmo.micro_utils.ksp.variations | ||||||
|  |  | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param argName New argument name. Default - empty - means "use default arg name" | ||||||
|  |  * @param type Qualified class name, like "dev.inmo.micro_utils.ksp.variants.GenerationVariant" | ||||||
|  |  * @param conversion Conversion string with `this` | ||||||
|  |  */ | ||||||
|  | @Retention(AnnotationRetention.BINARY) | ||||||
|  | @Repeatable | ||||||
|  | @Target(AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER) | ||||||
|  | annotation class GenerationVariant( | ||||||
|  |     val type: KClass<*>, | ||||||
|  |     val conversion: String, | ||||||
|  |     val argName: String = "", | ||||||
|  |     vararg val genericTypes: KClass<*> | ||||||
|  | ) | ||||||
| @@ -2,7 +2,7 @@ package dev.inmo.micro_utils.ktor.client | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.MPPFile | import dev.inmo.micro_utils.common.MPPFile | ||||||
| import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob | import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.ktor.common.TemporalFileId | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
| import io.ktor.client.HttpClient | import io.ktor.client.HttpClient | ||||||
| import io.ktor.client.content.* | import io.ktor.client.content.* | ||||||
| @@ -27,7 +27,7 @@ suspend fun tempUpload( | |||||||
|     val request = XMLHttpRequest() |     val request = XMLHttpRequest() | ||||||
|     request.responseType = XMLHttpRequestResponseType.TEXT |     request.responseType = XMLHttpRequestResponseType.TEXT | ||||||
|     request.upload.onprogress = { |     request.upload.onprogress = { | ||||||
|         subscope.launchSafelyWithoutExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) } |         subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) } | ||||||
|     } |     } | ||||||
|     request.onload = { |     request.onload = { | ||||||
|         if (request.status == 200.toShort()) { |         if (request.status == 200.toShort()) { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ package dev.inmo.micro_utils.ktor.client | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.MPPFile | import dev.inmo.micro_utils.common.MPPFile | ||||||
| import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob | import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import io.ktor.client.HttpClient | import io.ktor.client.HttpClient | ||||||
| import io.ktor.client.content.* | import io.ktor.client.content.* | ||||||
| import io.ktor.http.Headers | import io.ktor.http.Headers | ||||||
| @@ -66,7 +66,7 @@ actual suspend fun <T> HttpClient.uniUpload( | |||||||
|     } |     } | ||||||
|     request.responseType = XMLHttpRequestResponseType.TEXT |     request.responseType = XMLHttpRequestResponseType.TEXT | ||||||
|     request.upload.onprogress = { |     request.upload.onprogress = { | ||||||
|         subscope.launchSafelyWithoutExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) } |         subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) } | ||||||
|     } |     } | ||||||
|     request.onload = { |     request.onload = { | ||||||
|         if (request.status == 200.toShort()) { |         if (request.status == 200.toShort()) { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ package dev.inmo.micro_utils.ktor.server | |||||||
| import com.benasher44.uuid.uuid4 | import com.benasher44.uuid.uuid4 | ||||||
| import dev.inmo.micro_utils.common.FileName | import dev.inmo.micro_utils.common.FileName | ||||||
| import dev.inmo.micro_utils.common.MPPFile | import dev.inmo.micro_utils.common.MPPFile | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath | import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath | ||||||
| import dev.inmo.micro_utils.ktor.common.TemporalFileId | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
| import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator | import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator | ||||||
| @@ -44,7 +44,7 @@ class TemporalFilesRoutingConfigurator( | |||||||
|                     filesMap: MutableMap<TemporalFileId, MPPFile>, |                     filesMap: MutableMap<TemporalFileId, MPPFile>, | ||||||
|                     filesMutex: Mutex, |                     filesMutex: Mutex, | ||||||
|                     onNewFileFlow: Flow<TemporalFileId> |                     onNewFileFlow: Flow<TemporalFileId> | ||||||
|                 ): Job = scope.launchSafelyWithoutExceptions { |                 ): Job = scope.launchLoggingDropExceptions { | ||||||
|                     while (currentCoroutineContext().isActive) { |                     while (currentCoroutineContext().isActive) { | ||||||
|                         val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) -> |                         val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) -> | ||||||
|                             fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null) |                             fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null) | ||||||
|   | |||||||
| @@ -40,6 +40,8 @@ fun Pagination.intersect( | |||||||
| inline val Pagination.isFirstPage | inline val Pagination.isFirstPage | ||||||
|     get() = page == 0 |     get() = page == 0 | ||||||
|  |  | ||||||
|  | fun Pagination.firstPage() = if (isFirstPage) this else SimplePagination(0, size) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * First number in index of objects. It can be used as offset for databases or other data sources |  * First number in index of objects. It can be used as offset for databases or other data sources | ||||||
|  */ |  */ | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ data class PaginationResult<T>( | |||||||
|         page: Int, |         page: Int, | ||||||
|         results: List<T>, |         results: List<T>, | ||||||
|         pagesNumber: Int, |         pagesNumber: Int, | ||||||
|         size: Int |         size: Int = results.size | ||||||
|     ) : this( |     ) : this( | ||||||
|         page, |         page, | ||||||
|         size, |         size, | ||||||
|   | |||||||
| @@ -26,6 +26,16 @@ inline fun Pagination.nextPage() = | |||||||
|         size |         size | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method DO NOT check [Pagination.page] of receiver. Returns pagination for previous page | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun Pagination.previousPage() = | ||||||
|  |     SimplePagination( | ||||||
|  |         page - 1, | ||||||
|  |         size | ||||||
|  |     ) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param page Current page number |  * @param page Current page number | ||||||
|  * @param size Current page size |  * @param size Current page size | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								pagination/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								pagination/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  |     alias(libs.plugins.kt.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.pagination.common") | ||||||
|  |                 api project(":micro_utils.common.compose") | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,138 @@ | |||||||
|  | package dev.inmo.micro_utils.pagination.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow | ||||||
|  | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
|  | import dev.inmo.micro_utils.coroutines.runCatchingLogging | ||||||
|  | import dev.inmo.micro_utils.pagination.* | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Context for managing infinite pagination in a Compose UI. | ||||||
|  |  * | ||||||
|  |  * @param T The type of the data being paginated. | ||||||
|  |  * @property iterationState Holds the current pagination state and iteration count. | ||||||
|  |  * @property dataState Stores the loaded data, initially null. | ||||||
|  |  * @constructor Internal constructor to initialize pagination. | ||||||
|  |  * @param page Initial page number. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  */ | ||||||
|  | class InfinityPagedComponentContext<T> internal constructor( | ||||||
|  |     page: Int, | ||||||
|  |     size: Int, | ||||||
|  |     private val scope: CoroutineScope, | ||||||
|  |     private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T> | ||||||
|  | ) { | ||||||
|  |     internal val startPage = SimplePagination(page, size) | ||||||
|  |     internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null) | ||||||
|  |     internal val dataState = SpecialMutableStateFlow<List<T>?>(null) | ||||||
|  |     internal var loadingJob: Job? = null | ||||||
|  |     internal val loadingMutex = Mutex() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Loads the next page of data. If the current page is the last one, the function returns early. | ||||||
|  |      */ | ||||||
|  |     fun loadNext(): Job { | ||||||
|  |         return scope.launchLoggingDropExceptions { | ||||||
|  |             loadingMutex.withLock { | ||||||
|  |                 if (latestLoadedPage.value ?.isLastPage == true) return@launchLoggingDropExceptions | ||||||
|  |                 loadingJob = loadingJob ?: scope.launchLoggingDropExceptions { | ||||||
|  |                     runCatching { | ||||||
|  |                         loader(latestLoadedPage.value ?.nextPage() ?: startPage) | ||||||
|  |                     }.onSuccess { | ||||||
|  |                         latestLoadedPage.value = it | ||||||
|  |                         dataState.value = (dataState.value ?: emptyList()) + it.results | ||||||
|  |                     } | ||||||
|  |                     loadingMutex.withLock { | ||||||
|  |                         loadingJob = null | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 loadingJob | ||||||
|  |             } ?.join() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Reloads the pagination from the first page, clearing previously loaded data. | ||||||
|  |      */ | ||||||
|  |     fun reload(): Job { | ||||||
|  |         latestLoadedPage.value = null | ||||||
|  |         dataState.value = null | ||||||
|  |         return loadNext() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Composable function for managing an infinitely paged component. | ||||||
|  |  * | ||||||
|  |  * @param T The type of the paginated data. | ||||||
|  |  * @param page Initial page number. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will | ||||||
|  |  * receive null as `it` parameter | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | internal fun <T> InfinityPagedComponent( | ||||||
|  |     page: Int, | ||||||
|  |     size: Int, | ||||||
|  |     loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit | ||||||
|  | ) { | ||||||
|  |     val scope = predefinedScope ?: rememberCoroutineScope() | ||||||
|  |     val context = remember { InfinityPagedComponentContext<T>(page, size, scope, loader) } | ||||||
|  |     remember { | ||||||
|  |         context.reload() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val dataState = context.dataState.collectAsState() | ||||||
|  |     context.block(dataState.value) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Overloaded composable function for an infinitely paged component. | ||||||
|  |  * | ||||||
|  |  * @param T The type of the paginated data. | ||||||
|  |  * @param pageInfo Initial pagination information. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will | ||||||
|  |  * receive null as `it` parameter | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> InfinityPagedComponent( | ||||||
|  |     pageInfo: Pagination, | ||||||
|  |     loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit | ||||||
|  | ) { | ||||||
|  |     InfinityPagedComponent( | ||||||
|  |         pageInfo.page, | ||||||
|  |         pageInfo.size, | ||||||
|  |         loader, | ||||||
|  |         predefinedScope, | ||||||
|  |         block | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Overloaded composable function for an infinitely paged component. | ||||||
|  |  * | ||||||
|  |  * @param T The type of the paginated data. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will | ||||||
|  |  * receive null as `it` parameter | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> InfinityPagedComponent( | ||||||
|  |     size: Int, | ||||||
|  |     loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit | ||||||
|  | ) { | ||||||
|  |     InfinityPagedComponent(0, size, loader, predefinedScope, block) | ||||||
|  | } | ||||||
							
								
								
									
										166
									
								
								pagination/compose/src/commonMain/kotlin/PagedComponent.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								pagination/compose/src/commonMain/kotlin/PagedComponent.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | package dev.inmo.micro_utils.pagination.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow | ||||||
|  | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
|  | import dev.inmo.micro_utils.pagination.* | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Context for managing paginated data in a Compose UI. | ||||||
|  |  * | ||||||
|  |  * @param T The type of data being paginated. | ||||||
|  |  * @property iterationState Holds the current pagination state and iteration count. | ||||||
|  |  * @property dataOptional Stores the optional preloaded pagination result. | ||||||
|  |  * @property dataState Stores the current pagination result. | ||||||
|  |  * @constructor Internal constructor for setting up pagination. | ||||||
|  |  * @param preset Optional preset pagination result. | ||||||
|  |  * @param initialPage Initial page number. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  */ | ||||||
|  | class PagedComponentContext<T> internal constructor( | ||||||
|  |     initialPage: Int, | ||||||
|  |     size: Int, | ||||||
|  |     private val scope: CoroutineScope, | ||||||
|  |     private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T> | ||||||
|  | ) { | ||||||
|  |     internal val startPage = SimplePagination(initialPage, size) | ||||||
|  |     internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null) | ||||||
|  |     internal val dataState = SpecialMutableStateFlow<PaginationResult<T>?>(null) | ||||||
|  |     internal var loadingJob: Job? = null | ||||||
|  |     internal val loadingMutex = Mutex() | ||||||
|  |  | ||||||
|  |     private fun initLoadingJob( | ||||||
|  |         skipCheckerInLock: () -> Boolean, | ||||||
|  |         pageGetter: () -> Pagination | ||||||
|  |     ): Job { | ||||||
|  |         return scope.launchLoggingDropExceptions { | ||||||
|  |             loadingMutex.withLock { | ||||||
|  |                 if (skipCheckerInLock()) return@launchLoggingDropExceptions | ||||||
|  |                 loadingJob = loadingJob ?: scope.launchLoggingDropExceptions { | ||||||
|  |                     runCatching { | ||||||
|  |                         loader(pageGetter()) | ||||||
|  |                     }.onSuccess { | ||||||
|  |                         latestLoadedPage.value = it | ||||||
|  |                         dataState.value = it | ||||||
|  |                     } | ||||||
|  |                     loadingMutex.withLock { | ||||||
|  |                         loadingJob = null | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 loadingJob | ||||||
|  |             } ?.join() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Loads the next page of data. If the last page is reached, this function returns early. | ||||||
|  |      */ | ||||||
|  |     fun loadNext(): Job { | ||||||
|  |         return initLoadingJob( | ||||||
|  |             { latestLoadedPage.value ?.isLastPage == true } | ||||||
|  |         ) { | ||||||
|  |             latestLoadedPage.value ?.nextPage() ?: startPage | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Loads the previous page of data if available. | ||||||
|  |      */ | ||||||
|  |     fun loadPrevious(): Job { | ||||||
|  |         return initLoadingJob( | ||||||
|  |             { latestLoadedPage.value ?.isFirstPage == true } | ||||||
|  |         ) { | ||||||
|  |             latestLoadedPage.value ?.previousPage() ?: startPage | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Reloads the current page, refreshing the data. | ||||||
|  |      */ | ||||||
|  |     fun reload(): Job { | ||||||
|  |         return initLoadingJob( | ||||||
|  |             { | ||||||
|  |                 latestLoadedPage.value = null | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |         ) { | ||||||
|  |             startPage | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Composable function for paginated data displaying in a Compose UI. | ||||||
|  |  * | ||||||
|  |  * @param T The type of paginated data. | ||||||
|  |  * @param preload Optional preloaded pagination result. | ||||||
|  |  * @param initialPage Initial page number. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | internal fun <T> PagedComponent( | ||||||
|  |     initialPage: Int, | ||||||
|  |     size: Int, | ||||||
|  |     loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit | ||||||
|  | ) { | ||||||
|  |     val scope = predefinedScope ?: rememberCoroutineScope() | ||||||
|  |     val context = remember { PagedComponentContext<T>(initialPage, size, scope, loader) } | ||||||
|  |     remember { | ||||||
|  |         context.reload() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val pageState = context.dataState.collectAsState() | ||||||
|  |     pageState.value ?.let { | ||||||
|  |         context.block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Overloaded composable function for paginated components with pagination info. | ||||||
|  |  * | ||||||
|  |  * @param T The type of paginated data. | ||||||
|  |  * @param pageInfo Initial pagination information. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> PagedComponent( | ||||||
|  |     pageInfo: Pagination, | ||||||
|  |     loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit | ||||||
|  | ) { | ||||||
|  |     PagedComponent( | ||||||
|  |         pageInfo.page, | ||||||
|  |         pageInfo.size, | ||||||
|  |         loader, | ||||||
|  |         predefinedScope, | ||||||
|  |         block | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Overloaded composable function for paginated components with only a size parameter. | ||||||
|  |  * | ||||||
|  |  * @param T The type of paginated data. | ||||||
|  |  * @param size Number of items per page. | ||||||
|  |  * @param loader Suspended function that loads paginated data. | ||||||
|  |  * @param block Composable function that renders the UI with the loaded data. | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> PagedComponent( | ||||||
|  |     size: Int, | ||||||
|  |     loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>, | ||||||
|  |     predefinedScope: CoroutineScope? = null, | ||||||
|  |     block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit | ||||||
|  | ) { | ||||||
|  |     PagedComponent(0, size, loader, predefinedScope, block) | ||||||
|  | } | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.ui.test.ExperimentalTestApi | ||||||
|  | import androidx.compose.ui.test.runComposeUiTest | ||||||
|  | import dev.inmo.micro_utils.pagination.* | ||||||
|  | import dev.inmo.micro_utils.pagination.compose.InfinityPagedComponent | ||||||
|  | import dev.inmo.micro_utils.pagination.compose.PagedComponent | ||||||
|  | import org.jetbrains.annotations.TestOnly | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertContentEquals | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class InfinityPagedComponentTests { | ||||||
|  |     @OptIn(ExperimentalTestApi::class) | ||||||
|  |     @Test | ||||||
|  |     @TestOnly | ||||||
|  |     fun testSimpleLoad() = runComposeUiTest { | ||||||
|  |         var expectedList = listOf<Int>() | ||||||
|  |         setContent { | ||||||
|  |             InfinityPagedComponent<Int>( | ||||||
|  |                 size = 1, | ||||||
|  |                 loader = { | ||||||
|  |                     PaginationResult( | ||||||
|  |                         page = it.page, | ||||||
|  |                         size = it.size, | ||||||
|  |                         results = (it.firstIndex .. it.lastIndex).toList(), | ||||||
|  |                         objectsNumber = 3 | ||||||
|  |                     ).also { | ||||||
|  |                         expectedList += it.results | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ) { | ||||||
|  |                 if (it == null) { | ||||||
|  |                     assertEquals(null, it) | ||||||
|  |                 } else { | ||||||
|  |                     assertEquals(expectedList, it) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 LaunchedEffect(it ?.size) { | ||||||
|  |                     loadNext().join() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertContentEquals( | ||||||
|  |             listOf(0, 1, 2), | ||||||
|  |             expectedList | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.ui.test.ExperimentalTestApi | ||||||
|  | import androidx.compose.ui.test.runComposeUiTest | ||||||
|  | import dev.inmo.micro_utils.pagination.* | ||||||
|  | import dev.inmo.micro_utils.pagination.compose.PagedComponent | ||||||
|  | import org.jetbrains.annotations.TestOnly | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class PagedComponentTests { | ||||||
|  |     @OptIn(ExperimentalTestApi::class) | ||||||
|  |     @Test | ||||||
|  |     @TestOnly | ||||||
|  |     fun testSimpleLoad() = runComposeUiTest { | ||||||
|  |         var expectedPage = PaginationResult( | ||||||
|  |             page = 0, | ||||||
|  |             size = 1, | ||||||
|  |             results = listOf(0), | ||||||
|  |             objectsNumber = 3 | ||||||
|  |         ) | ||||||
|  |         var previousPage = expectedPage | ||||||
|  |         setContent { | ||||||
|  |             PagedComponent<Int>( | ||||||
|  |                 initialPage = 0, | ||||||
|  |                 size = 1, | ||||||
|  |                 loader = { | ||||||
|  |                     previousPage = expectedPage | ||||||
|  |                     expectedPage = PaginationResult( | ||||||
|  |                         page = it.page, | ||||||
|  |                         size = it.size, | ||||||
|  |                         results = (it.firstIndex .. it.lastIndex).toList(), | ||||||
|  |                         objectsNumber = 3 | ||||||
|  |                     ) | ||||||
|  |                     expectedPage | ||||||
|  |                 } | ||||||
|  |             ) { | ||||||
|  |                 assertEquals(expectedPage, it) | ||||||
|  |                 assertEquals(expectedPage.results, it.results) | ||||||
|  |  | ||||||
|  |                 if (it.isLastPage || it.page < previousPage.page) { | ||||||
|  |                     if (it.isFirstPage) { | ||||||
|  |                         // do nothing - end of test | ||||||
|  |                     } else { | ||||||
|  |                         loadPrevious() | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     loadNext() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertEquals( | ||||||
|  |             PaginationResult( | ||||||
|  |                 page = 0, | ||||||
|  |                 size = 1, | ||||||
|  |                 results = listOf(0), | ||||||
|  |                 objectsNumber = 3 | ||||||
|  |             ), | ||||||
|  |             expectedPage | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,19 +2,19 @@ package dev.inmo.micro_utils.pagination | |||||||
|  |  | ||||||
| import org.jetbrains.exposed.sql.* | import org.jetbrains.exposed.sql.* | ||||||
|  |  | ||||||
| fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) = limit( | fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) = | ||||||
|     with.size, |     limit(with.size) | ||||||
|     with.firstIndex.toLong() |     .offset(with.firstIndex.toLong()) | ||||||
| ).let { |     .let { | ||||||
|     if (orderBy != null) { |         if (orderBy != null) { | ||||||
|         it.orderBy( |             it.orderBy( | ||||||
|             orderBy.first, |                 orderBy.first, | ||||||
|             orderBy.second |                 orderBy.second | ||||||
|         ) |             ) | ||||||
|     } else { |         } else { | ||||||
|         it |             it | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  |  | ||||||
| fun Query.paginate(with: Pagination, orderBy: Expression<*>?, reversed: Boolean = false) = paginate( | fun Query.paginate(with: Pagination, orderBy: Expression<*>?, reversed: Boolean = false) = paginate( | ||||||
|     with, |     with, | ||||||
|   | |||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.repos.annotations | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @RequiresOptIn( | ||||||
|  |     "Overriding of this invalidate message requires manual launching of invalidation on class initialization process", | ||||||
|  |     RequiresOptIn.Level.WARNING | ||||||
|  | ) | ||||||
|  | @Target( | ||||||
|  |     AnnotationTarget.CONSTRUCTOR, | ||||||
|  |     AnnotationTarget.FIELD, | ||||||
|  |     AnnotationTarget.PROPERTY, | ||||||
|  |     AnnotationTarget.FUNCTION, | ||||||
|  | ) | ||||||
|  | annotation class OverrideRequireManualInvalidation | ||||||
| @@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.SmartRWLocker | |||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.cache.KVCache | import dev.inmo.micro_utils.repos.cache.cache.KVCache | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| @@ -12,10 +13,10 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class ReadCRUDCacheRepo<ObjectType, IdType>( | open class ReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>, |     protected val parentRepo: ReadCRUDRepo<ObjectType, IdType>, | ||||||
|     protected open val kvCache: KVCache<IdType, ObjectType>, |     protected val kvCache: KVCache<IdType, ObjectType>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     protected open val idGetter: (ObjectType) -> IdType |     protected val idGetter: (ObjectType) -> IdType | ||||||
| ) : ReadCRUDRepo<ObjectType, IdType> by parentRepo, CommonCacheRepo { | ) : ReadCRUDRepo<ObjectType, IdType> by parentRepo, CommonCacheRepo { | ||||||
|     override suspend fun getById(id: IdType): ObjectType? = locker.withReadAcquire { |     override suspend fun getById(id: IdType): ObjectType? = locker.withReadAcquire { | ||||||
|         kvCache.get(id) |         kvCache.get(id) | ||||||
| @@ -39,6 +40,7 @@ open class ReadCRUDCacheRepo<ObjectType, IdType>( | |||||||
|         kvCache.contains(id) |         kvCache.contains(id) | ||||||
|     } || parentRepo.contains(id) |     } || parentRepo.contains(id) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() = locker.withWriteLock { |     override suspend fun invalidate() = locker.withWriteLock { | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
| @@ -51,11 +53,11 @@ fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached( | |||||||
| ) = ReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ) = ReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ||||||
|  |  | ||||||
| open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>( | open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>( | ||||||
|     protected open val parentRepo: WriteCRUDRepo<ObjectType, IdType, InputValueType>, |     protected val parentRepo: WriteCRUDRepo<ObjectType, IdType, InputValueType>, | ||||||
|     protected open val kvCache: KeyValueRepo<IdType, ObjectType>, |     protected val kvCache: KeyValueRepo<IdType, ObjectType>, | ||||||
|     protected open val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     protected val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     protected open val idGetter: (ObjectType) -> IdType |     protected val idGetter: (ObjectType) -> IdType | ||||||
| ) : WriteCRUDRepo<ObjectType, IdType, InputValueType>, CommonCacheRepo { | ) : WriteCRUDRepo<ObjectType, IdType, InputValueType>, CommonCacheRepo { | ||||||
|     override val newObjectsFlow: Flow<ObjectType> by parentRepo::newObjectsFlow |     override val newObjectsFlow: Flow<ObjectType> by parentRepo::newObjectsFlow | ||||||
|     override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow |     override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow | ||||||
| @@ -117,6 +119,7 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|         return created |         return created | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() = locker.withWriteLock { |     override suspend fun invalidate() = locker.withWriteLock { | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
| @@ -131,25 +134,26 @@ fun <ObjectType, IdType, InputType> WriteCRUDRepo<ObjectType, IdType, InputType> | |||||||
|  |  | ||||||
|  |  | ||||||
| open class CRUDCacheRepo<ObjectType, IdType, InputValueType>( | open class CRUDCacheRepo<ObjectType, IdType, InputValueType>( | ||||||
|     override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>, |     protected val crudRepo: CRUDRepo<ObjectType, IdType, InputValueType>, | ||||||
|     kvCache: KVCache<IdType, ObjectType>, |     kvCache: KVCache<IdType, ObjectType>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     locker: SmartRWLocker = SmartRWLocker(), |     locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     idGetter: (ObjectType) -> IdType |     idGetter: (ObjectType) -> IdType | ||||||
| ) : ReadCRUDCacheRepo<ObjectType, IdType>( | ) : ReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     parentRepo, |     crudRepo, | ||||||
|     kvCache, |     kvCache, | ||||||
|     locker, |     locker, | ||||||
|     idGetter |     idGetter | ||||||
| ), | ), | ||||||
|     WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( | WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( | ||||||
|     parentRepo, |     crudRepo, | ||||||
|     kvCache, |     kvCache, | ||||||
|     scope, |     scope, | ||||||
|     locker, |     locker, | ||||||
|     idGetter |     idGetter | ||||||
| ), | ), | ||||||
|     CRUDRepo<ObjectType, IdType, InputValueType> { | CRUDRepo<ObjectType, IdType, InputValueType> { | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) |     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,8 @@ | |||||||
| package dev.inmo.micro_utils.repos.cache | package dev.inmo.micro_utils.repos.cache | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  |  | ||||||
| interface InvalidatableRepo { | interface InvalidatableRepo { | ||||||
|     /** |     /** | ||||||
|      * Invalidates its internal data. It __may__ lead to autoreload of data. In case when repo makes autoreload, |      * Invalidates its internal data. It __may__ lead to autoreload of data. In case when repo makes autoreload, | ||||||
| @@ -8,4 +11,14 @@ interface InvalidatableRepo { | |||||||
|     suspend fun invalidate() |     suspend fun invalidate() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun <T : InvalidatableRepo> T.alsoInvalidate() = also { | ||||||
|  |     invalidate() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T : InvalidatableRepo> T.alsoInvalidateAsync(scope: CoroutineScope) = also { | ||||||
|  |     scope.launchLoggingDropExceptions { | ||||||
|  |         invalidate() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| typealias CacheRepo = InvalidatableRepo | typealias CacheRepo = InvalidatableRepo | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import dev.inmo.micro_utils.coroutines.withReadAcquire | |||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.cache.KVCache | import dev.inmo.micro_utils.repos.cache.cache.KVCache | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -12,8 +13,8 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class ReadKeyValueCacheRepo<Key,Value>( | open class ReadKeyValueCacheRepo<Key,Value>( | ||||||
|     protected open val parentRepo: ReadKeyValueRepo<Key, Value>, |     protected val parentRepo: ReadKeyValueRepo<Key, Value>, | ||||||
|     protected open val kvCache: KVCache<Key, Value>, |     protected val kvCache: KVCache<Key, Value>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : ReadKeyValueRepo<Key,Value> by parentRepo, CommonCacheRepo { | ) : ReadKeyValueRepo<Key,Value> by parentRepo, CommonCacheRepo { | ||||||
|     override suspend fun get(k: Key): Value? = locker.withReadAcquire { |     override suspend fun get(k: Key): Value? = locker.withReadAcquire { | ||||||
| @@ -49,6 +50,7 @@ open class ReadKeyValueCacheRepo<Key,Value>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) |     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -58,24 +60,24 @@ fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached( | |||||||
| ) = ReadKeyValueCacheRepo(this, kvCache, locker) | ) = ReadKeyValueCacheRepo(this, kvCache, locker) | ||||||
|  |  | ||||||
| open class KeyValueCacheRepo<Key,Value>( | open class KeyValueCacheRepo<Key,Value>( | ||||||
|     override val parentRepo: KeyValueRepo<Key, Value>, |     protected val kvRepo: KeyValueRepo<Key, Value>, | ||||||
|     kvCache: KVCache<Key, Value>, |     kvCache: KVCache<Key, Value>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     locker: SmartRWLocker = SmartRWLocker(), |     locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, locker), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo, CommonCacheRepo { | ) : ReadKeyValueCacheRepo<Key,Value>(kvRepo, kvCache, locker), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by kvRepo, CommonCacheRepo { | ||||||
|     protected val onNewJob = parentRepo.onNewValue.onEach { |     protected val onNewJob = kvRepo.onNewValue.onEach { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             kvCache.set(it.first, it.second) |             kvCache.set(it.first, it.second) | ||||||
|         } |         } | ||||||
|     }.launchIn(scope) |     }.launchIn(scope) | ||||||
|     protected val onRemoveJob = parentRepo.onValueRemoved.onEach { |     protected val onRemoveJob = kvRepo.onValueRemoved.onEach { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             kvCache.unset(it) |             kvCache.unset(it) | ||||||
|         } |         } | ||||||
|     }.launchIn(scope) |     }.launchIn(scope) | ||||||
|  |  | ||||||
|     override suspend fun clear() { |     override suspend fun clear() { | ||||||
|         parentRepo.clear() |         kvRepo.clear() | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             kvCache.clear() |             kvCache.clear() | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock | |||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.pagination.utils.* | import dev.inmo.micro_utils.pagination.utils.* | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.cache.KVCache | import dev.inmo.micro_utils.repos.cache.cache.KVCache | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -13,8 +14,8 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class ReadKeyValuesCacheRepo<Key,Value>( | open class ReadKeyValuesCacheRepo<Key,Value>( | ||||||
|     protected open val parentRepo: ReadKeyValuesRepo<Key, Value>, |     protected val parentRepo: ReadKeyValuesRepo<Key, Value>, | ||||||
|     protected open val kvCache: KVCache<Key, List<Value>>, |     protected val kvCache: KVCache<Key, List<Value>>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : ReadKeyValuesRepo<Key,Value> by parentRepo, CommonCacheRepo { | ) : ReadKeyValuesRepo<Key,Value> by parentRepo, CommonCacheRepo { | ||||||
|     override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> { |     override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> { | ||||||
| @@ -48,6 +49,7 @@ open class ReadKeyValuesCacheRepo<Key,Value>( | |||||||
|         kvCache.contains(k) |         kvCache.contains(k) | ||||||
|     } || parentRepo.contains(k) |     } || parentRepo.contains(k) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) |     override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import dev.inmo.micro_utils.pagination.PaginationResult | |||||||
| import dev.inmo.micro_utils.repos.KeyValueRepo | import dev.inmo.micro_utils.repos.KeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.MapKeyValueRepo | import dev.inmo.micro_utils.repos.MapKeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.ReadCRUDRepo | import dev.inmo.micro_utils.repos.ReadCRUDRepo | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch | |||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
| open class AutoRecacheReadCRUDRepo<RegisteredObject, Id>( | open class AutoRecacheReadCRUDRepo<RegisteredObject, Id>( | ||||||
|     protected open val originalRepo: ReadCRUDRepo<RegisteredObject, Id>, |     protected val originalRepo: ReadCRUDRepo<RegisteredObject, Id>, | ||||||
|     protected val scope: CoroutineScope, |     protected val scope: CoroutineScope, | ||||||
|     protected val kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), |     protected val kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), | ||||||
|     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, |     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, | ||||||
| @@ -90,6 +91,7 @@ open class AutoRecacheReadCRUDRepo<RegisteredObject, Id>( | |||||||
|         kvCache.set(idGetter(it), it) |         kvCache.set(idGetter(it), it) | ||||||
|     } ?: kvCache.get(id) |     } ?: kvCache.get(id) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.crud | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| @@ -53,6 +54,7 @@ open class AutoRecacheWriteCRUDRepo<RegisteredObject, Id, InputObject>( | |||||||
|         kvCache.set(idGetter(it), it) |         kvCache.set(idGetter(it), it) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,21 +8,21 @@ import kotlinx.coroutines.CoroutineScope | |||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
| open class AutoRecacheKeyValueRepo<Id, RegisteredObject>( | open class AutoRecacheKeyValueRepo<Id, RegisteredObject>( | ||||||
|     override val originalRepo: KeyValueRepo<Id, RegisteredObject>, |     protected val kvRepo: KeyValueRepo<Id, RegisteredObject>, | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), |     kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), | ||||||
|     recacheDelay: Long = 60.seconds.inWholeMilliseconds, |     recacheDelay: Long = 60.seconds.inWholeMilliseconds, | ||||||
|     actionWrapper: ActionWrapper = ActionWrapper.Direct, |     actionWrapper: ActionWrapper = ActionWrapper.Direct, | ||||||
|     idGetter: (RegisteredObject) -> Id |     idGetter: (RegisteredObject) -> Id | ||||||
| ) : AutoRecacheReadKeyValueRepo<Id, RegisteredObject> ( | ) : AutoRecacheReadKeyValueRepo<Id, RegisteredObject> ( | ||||||
|     originalRepo, |     kvRepo, | ||||||
|     scope, |     scope, | ||||||
|     kvCache, |     kvCache, | ||||||
|     recacheDelay, |     recacheDelay, | ||||||
|     actionWrapper, |     actionWrapper, | ||||||
|     idGetter |     idGetter | ||||||
| ), | ), | ||||||
|     WriteKeyValueRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValueRepo(originalRepo, scope, kvCache), |     WriteKeyValueRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValueRepo(kvRepo, scope, kvCache), | ||||||
|     KeyValueRepo<Id, RegisteredObject> { |     KeyValueRepo<Id, RegisteredObject> { | ||||||
|  |  | ||||||
|     constructor( |     constructor( | ||||||
| @@ -34,14 +34,14 @@ open class AutoRecacheKeyValueRepo<Id, RegisteredObject>( | |||||||
|         idGetter: (RegisteredObject) -> Id |         idGetter: (RegisteredObject) -> Id | ||||||
|     ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) |     ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) | ||||||
|  |  | ||||||
|     override suspend fun unsetWithValues(toUnset: List<RegisteredObject>) = originalRepo.unsetWithValues( |     override suspend fun unsetWithValues(toUnset: List<RegisteredObject>) = kvRepo.unsetWithValues( | ||||||
|         toUnset |         toUnset | ||||||
|     ).also { |     ).also { | ||||||
|         kvCache.unsetWithValues(toUnset) |         kvCache.unsetWithValues(toUnset) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun clear() { |     override suspend fun clear() { | ||||||
|         originalRepo.clear() |         kvRepo.clear() | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import dev.inmo.micro_utils.pagination.PaginationResult | |||||||
| import dev.inmo.micro_utils.repos.KeyValueRepo | import dev.inmo.micro_utils.repos.KeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.MapKeyValueRepo | import dev.inmo.micro_utils.repos.MapKeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.ReadKeyValueRepo | import dev.inmo.micro_utils.repos.ReadKeyValueRepo | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch | |||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
| open class AutoRecacheReadKeyValueRepo<Id, RegisteredObject>( | open class AutoRecacheReadKeyValueRepo<Id, RegisteredObject>( | ||||||
|     protected open val originalRepo: ReadKeyValueRepo<Id, RegisteredObject>, |     protected val originalRepo: ReadKeyValueRepo<Id, RegisteredObject>, | ||||||
|     protected val scope: CoroutineScope, |     protected val scope: CoroutineScope, | ||||||
|     protected val kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), |     protected val kvCache: KeyValueRepo<Id, RegisteredObject> = MapKeyValueRepo(), | ||||||
|     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, |     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, | ||||||
| @@ -100,6 +101,7 @@ open class AutoRecacheReadKeyValueRepo<Id, RegisteredObject>( | |||||||
|         originalRepo.keys(v, pagination, reversed) |         originalRepo.keys(v, pagination, reversed) | ||||||
|     }.getOrElse { kvCache.keys(v, pagination, reversed) } |     }.getOrElse { kvCache.keys(v, pagination, reversed) } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.keyvalue | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| @@ -44,6 +45,7 @@ open class AutoRecacheWriteKeyValueRepo<Id, RegisteredObject>( | |||||||
|         kvCache.set(toSet) |         kvCache.set(toSet) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,19 +9,19 @@ import kotlinx.coroutines.CoroutineScope | |||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
| open class AutoRecacheKeyValuesRepo<Id, RegisteredObject>( | open class AutoRecacheKeyValuesRepo<Id, RegisteredObject>( | ||||||
|     override val originalRepo: KeyValuesRepo<Id, RegisteredObject>, |     protected val kvsRepo: KeyValuesRepo<Id, RegisteredObject>, | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     kvCache: KeyValueRepo<Id, List<RegisteredObject>> = MapKeyValueRepo(), |     kvCache: KeyValueRepo<Id, List<RegisteredObject>> = MapKeyValueRepo(), | ||||||
|     recacheDelay: Long = 60.seconds.inWholeMilliseconds, |     recacheDelay: Long = 60.seconds.inWholeMilliseconds, | ||||||
|     actionWrapper: ActionWrapper = ActionWrapper.Direct |     actionWrapper: ActionWrapper = ActionWrapper.Direct | ||||||
| ) : AutoRecacheReadKeyValuesRepo<Id, RegisteredObject> ( | ) : AutoRecacheReadKeyValuesRepo<Id, RegisteredObject> ( | ||||||
|     originalRepo, |     kvsRepo, | ||||||
|     scope, |     scope, | ||||||
|     kvCache, |     kvCache, | ||||||
|     recacheDelay, |     recacheDelay, | ||||||
|     actionWrapper |     actionWrapper | ||||||
| ), | ), | ||||||
|     WriteKeyValuesRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValuesRepo(originalRepo, scope, kvCache), |     WriteKeyValuesRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValuesRepo(kvsRepo, scope, kvCache), | ||||||
|     KeyValuesRepo<Id, RegisteredObject> { |     KeyValuesRepo<Id, RegisteredObject> { | ||||||
|  |  | ||||||
|     constructor( |     constructor( | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import dev.inmo.micro_utils.pagination.utils.paginate | |||||||
| import dev.inmo.micro_utils.repos.KeyValueRepo | import dev.inmo.micro_utils.repos.KeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.MapKeyValueRepo | import dev.inmo.micro_utils.repos.MapKeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.ReadKeyValuesRepo | import dev.inmo.micro_utils.repos.ReadKeyValuesRepo | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| @@ -24,7 +25,7 @@ import kotlinx.coroutines.launch | |||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
|  |  | ||||||
| open class AutoRecacheReadKeyValuesRepo<Id, RegisteredObject>( | open class AutoRecacheReadKeyValuesRepo<Id, RegisteredObject>( | ||||||
|     protected open val originalRepo: ReadKeyValuesRepo<Id, RegisteredObject>, |     protected val originalRepo: ReadKeyValuesRepo<Id, RegisteredObject>, | ||||||
|     protected val scope: CoroutineScope, |     protected val scope: CoroutineScope, | ||||||
|     protected val kvCache: KeyValueRepo<Id, List<RegisteredObject>> = MapKeyValueRepo(), |     protected val kvCache: KeyValueRepo<Id, List<RegisteredObject>> = MapKeyValueRepo(), | ||||||
|     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, |     protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, | ||||||
| @@ -140,6 +141,7 @@ open class AutoRecacheReadKeyValuesRepo<Id, RegisteredObject>( | |||||||
|         }) ?: (kvCache.get(k) ?.contains(v) == true) |         }) ?: (kvCache.get(k) ?.contains(v) == true) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.keyvalues | |||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
| import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
| import dev.inmo.micro_utils.repos.pagination.maxPagePagination | import dev.inmo.micro_utils.repos.pagination.maxPagePagination | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -89,6 +90,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ package dev.inmo.micro_utils.repos.cache.full | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* | import dev.inmo.micro_utils.common.* | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.Pagination | import dev.inmo.micro_utils.pagination.Pagination | ||||||
| import dev.inmo.micro_utils.pagination.PaginationResult | import dev.inmo.micro_utils.pagination.PaginationResult | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.* | import dev.inmo.micro_utils.repos.cache.* | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| @@ -15,10 +16,10 @@ import kotlinx.coroutines.CoroutineScope | |||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
|  |  | ||||||
| open class FullReadCRUDCacheRepo<ObjectType, IdType>( | open class FullReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>, |     protected val parentRepo: ReadCRUDRepo<ObjectType, IdType>, | ||||||
|     protected open val kvCache: KeyValueRepo<IdType, ObjectType>, |     protected val kvCache: KeyValueRepo<IdType, ObjectType>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     protected open val idGetter: (ObjectType) -> IdType |     protected val idGetter: (ObjectType) -> IdType | ||||||
| ) : ReadCRUDRepo<ObjectType, IdType>, FullCacheRepo { | ) : ReadCRUDRepo<ObjectType, IdType>, FullCacheRepo { | ||||||
|     protected suspend inline fun <T> doOrTakeAndActualize( |     protected suspend inline fun <T> doOrTakeAndActualize( | ||||||
|         action: KeyValueRepo<IdType, ObjectType>.() -> Optional<T>, |         action: KeyValueRepo<IdType, ObjectType>.() -> Optional<T>, | ||||||
| @@ -94,20 +95,20 @@ fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached( | |||||||
| ) = FullReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ) = FullReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ||||||
|  |  | ||||||
| open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | ||||||
|     override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>, |     protected val crudRepo: CRUDRepo<ObjectType, IdType, InputValueType>, | ||||||
|     kvCache: KeyValueRepo<IdType, ObjectType>, |     kvCache: KeyValueRepo<IdType, ObjectType>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
|     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), |     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), | ||||||
|     idGetter: (ObjectType) -> IdType |     idGetter: (ObjectType) -> IdType | ||||||
| ) : FullReadCRUDCacheRepo<ObjectType, IdType>( | ) : FullReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     parentRepo, |     crudRepo, | ||||||
|     kvCache, |     kvCache, | ||||||
|     locker, |     locker, | ||||||
|     idGetter |     idGetter | ||||||
| ), | ), | ||||||
|     WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( |     WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( | ||||||
|         parentRepo, |         crudRepo, | ||||||
|         kvCache, |         kvCache, | ||||||
|         scope, |         scope, | ||||||
|         locker, |         locker, | ||||||
| @@ -116,7 +117,7 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|     CRUDRepo<ObjectType, IdType, InputValueType> { |     CRUDRepo<ObjectType, IdType, InputValueType> { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -128,11 +129,12 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|  |  | ||||||
|     protected open suspend fun initialInvalidate() { |     protected open suspend fun initialInvalidate() { | ||||||
|         try { |         try { | ||||||
|             kvCache.actualizeAll(parentRepo, locker = null) |             kvCache.actualizeAll(crudRepo, locker = null) | ||||||
|         } finally { |         } finally { | ||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ package dev.inmo.micro_utils.repos.cache.full | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* | import dev.inmo.micro_utils.common.* | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.Pagination | import dev.inmo.micro_utils.pagination.Pagination | ||||||
| import dev.inmo.micro_utils.pagination.PaginationResult | import dev.inmo.micro_utils.pagination.PaginationResult | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -15,8 +16,8 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class FullReadKeyValueCacheRepo<Key,Value>( | open class FullReadKeyValueCacheRepo<Key,Value>( | ||||||
|     protected open val parentRepo: ReadKeyValueRepo<Key, Value>, |     protected val parentRepo: ReadKeyValueRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, Value>, |     protected val kvCache: KeyValueRepo<Key, Value>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker() |     protected val locker: SmartRWLocker = SmartRWLocker() | ||||||
| ) : ReadKeyValueRepo<Key, Value>, FullCacheRepo { | ) : ReadKeyValueRepo<Key, Value>, FullCacheRepo { | ||||||
|     protected suspend inline fun <T> doOrTakeAndActualize( |     protected suspend inline fun <T> doOrTakeAndActualize( | ||||||
| @@ -86,6 +87,7 @@ open class FullReadKeyValueCacheRepo<Key,Value>( | |||||||
|         { if (it.results.isNotEmpty()) actualizeAll() } |         { if (it.results.isNotEmpty()) actualizeAll() } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
| @@ -98,7 +100,7 @@ fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached( | |||||||
|  |  | ||||||
| open class FullWriteKeyValueCacheRepo<Key,Value>( | open class FullWriteKeyValueCacheRepo<Key,Value>( | ||||||
|     parentRepo: WriteKeyValueRepo<Key, Value>, |     parentRepo: WriteKeyValueRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, Value>, |     protected val kvCache: KeyValueRepo<Key, Value>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker() |     protected val locker: SmartRWLocker = SmartRWLocker() | ||||||
| ) : WriteKeyValueRepo<Key, Value> by parentRepo, FullCacheRepo { | ) : WriteKeyValueRepo<Key, Value> by parentRepo, FullCacheRepo { | ||||||
| @@ -126,22 +128,22 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching( | |||||||
| ) = FullWriteKeyValueCacheRepo(this, kvCache, scope) | ) = FullWriteKeyValueCacheRepo(this, kvCache, scope) | ||||||
|  |  | ||||||
| open class FullKeyValueCacheRepo<Key,Value>( | open class FullKeyValueCacheRepo<Key,Value>( | ||||||
|     override val parentRepo: KeyValueRepo<Key, Value>, |     protected val kvRepo: KeyValueRepo<Key, Value>, | ||||||
|     kvCache: KeyValueRepo<Key, Value>, |     kvCache: KeyValueRepo<Key, Value>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
|     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), |     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), | ||||||
| ) : //FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope), | ) : //FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope), | ||||||
|     KeyValueRepo<Key,Value>, |     KeyValueRepo<Key,Value>, | ||||||
|     WriteKeyValueRepo<Key,Value> by parentRepo, |     WriteKeyValueRepo<Key,Value> by kvRepo, | ||||||
|     FullReadKeyValueCacheRepo<Key, Value>( |     FullReadKeyValueCacheRepo<Key, Value>( | ||||||
|         parentRepo, |         kvRepo, | ||||||
|         kvCache, |         kvCache, | ||||||
|         locker |         locker | ||||||
| ) { | ) { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -151,7 +153,7 @@ open class FullKeyValueCacheRepo<Key,Value>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset) |     override suspend fun unsetWithValues(toUnset: List<Value>) = kvRepo.unsetWithValues(toUnset) | ||||||
|  |  | ||||||
|     protected open suspend fun initialInvalidate() { |     protected open suspend fun initialInvalidate() { | ||||||
|         try { |         try { | ||||||
| @@ -160,18 +162,19 @@ open class FullKeyValueCacheRepo<Key,Value>( | |||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.actualizeAll(parentRepo, locker) |         kvCache.actualizeAll(parentRepo, locker) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun clear() { |     override suspend fun clear() { | ||||||
|         parentRepo.clear() |         kvRepo.clear() | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun set(toSet: Map<Key, Value>) { |     override suspend fun set(toSet: Map<Key, Value>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.set(toSet) |             kvRepo.set(toSet) | ||||||
|             kvCache.set( |             kvCache.set( | ||||||
|                 toSet.filter { |                 toSet.filter { | ||||||
|                     parentRepo.contains(it.key) |                     parentRepo.contains(it.key) | ||||||
| @@ -182,7 +185,7 @@ open class FullKeyValueCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun unset(toUnset: List<Key>) { |     override suspend fun unset(toUnset: List<Key>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.unset(toUnset) |             kvRepo.unset(toUnset) | ||||||
|             kvCache.unset( |             kvCache.unset( | ||||||
|                 toUnset.filter { |                 toUnset.filter { | ||||||
|                     !parentRepo.contains(it) |                     !parentRepo.contains(it) | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ package dev.inmo.micro_utils.repos.cache.full | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* | import dev.inmo.micro_utils.common.* | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.pagination.utils.* | import dev.inmo.micro_utils.pagination.utils.* | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import dev.inmo.micro_utils.repos.pagination.maxPagePagination | import dev.inmo.micro_utils.repos.pagination.maxPagePagination | ||||||
| @@ -16,8 +17,8 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class FullReadKeyValuesCacheRepo<Key,Value>( | open class FullReadKeyValuesCacheRepo<Key,Value>( | ||||||
|     protected open val parentRepo: ReadKeyValuesRepo<Key, Value>, |     protected val parentRepo: ReadKeyValuesRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, List<Value>>, |     protected val kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : ReadKeyValuesRepo<Key, Value>, FullCacheRepo { | ) : ReadKeyValuesRepo<Key, Value>, FullCacheRepo { | ||||||
|     protected suspend inline fun <T> doOrTakeAndActualize( |     protected suspend inline fun <T> doOrTakeAndActualize( | ||||||
| @@ -153,6 +154,7 @@ open class FullReadKeyValuesCacheRepo<Key,Value>( | |||||||
|         { if (it.results.isNotEmpty()) actualizeAll() } |         { if (it.results.isNotEmpty()) actualizeAll() } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
| @@ -165,7 +167,7 @@ fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached( | |||||||
|  |  | ||||||
| open class FullWriteKeyValuesCacheRepo<Key,Value>( | open class FullWriteKeyValuesCacheRepo<Key,Value>( | ||||||
|     parentRepo: WriteKeyValuesRepo<Key, Value>, |     parentRepo: WriteKeyValuesRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, List<Value>>, |     protected val kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : WriteKeyValuesRepo<Key, Value> by parentRepo, FullCacheRepo { | ) : WriteKeyValuesRepo<Key, Value> by parentRepo, FullCacheRepo { | ||||||
| @@ -200,17 +202,17 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching( | |||||||
| ) = FullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) | ) = FullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) | ||||||
|  |  | ||||||
| open class FullKeyValuesCacheRepo<Key,Value>( | open class FullKeyValuesCacheRepo<Key,Value>( | ||||||
|     override val parentRepo: KeyValuesRepo<Key, Value>, |     protected val kvsRepo: KeyValuesRepo<Key, Value>, | ||||||
|     kvCache: KeyValueRepo<Key, List<Value>>, |     kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
|     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), |     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), | ||||||
| ) : KeyValuesRepo<Key, Value>, | ) : KeyValuesRepo<Key, Value>, | ||||||
|     FullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker), |     FullReadKeyValuesCacheRepo<Key, Value>(kvsRepo, kvCache, locker), | ||||||
|     WriteKeyValuesRepo<Key, Value> by parentRepo { |     WriteKeyValuesRepo<Key, Value> by kvsRepo { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -235,13 +237,14 @@ open class FullKeyValuesCacheRepo<Key,Value>( | |||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.actualizeAll(parentRepo, locker = locker) |         kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun set(toSet: Map<Key, List<Value>>) { |     override suspend fun set(toSet: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.set(toSet) |             kvsRepo.set(toSet) | ||||||
|             kvCache.set( |             kvCache.set( | ||||||
|                 toSet.filter { |                 toSet.filter { | ||||||
|                     parentRepo.contains(it.key) |                     parentRepo.contains(it.key) | ||||||
| @@ -252,7 +255,7 @@ open class FullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun add(toAdd: Map<Key, List<Value>>) { |     override suspend fun add(toAdd: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.add(toAdd) |             kvsRepo.add(toAdd) | ||||||
|             toAdd.forEach { |             toAdd.forEach { | ||||||
|                 val filtered = it.value.filter { v -> |                 val filtered = it.value.filter { v -> | ||||||
|                     parentRepo.contains(it.key, v) |                     parentRepo.contains(it.key, v) | ||||||
| @@ -269,7 +272,7 @@ open class FullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun remove(toRemove: Map<Key, List<Value>>) { |     override suspend fun remove(toRemove: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.remove(toRemove) |             kvsRepo.remove(toRemove) | ||||||
|             toRemove.forEach { |             toRemove.forEach { | ||||||
|                 val filtered = it.value.filter { v -> |                 val filtered = it.value.filter { v -> | ||||||
|                     !parentRepo.contains(it.key, v) |                     !parentRepo.contains(it.key, v) | ||||||
| @@ -291,7 +294,7 @@ open class FullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun clear(k: Key) { |     override suspend fun clear(k: Key) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.clear(k) |             kvsRepo.clear(k) | ||||||
|             if (parentRepo.contains(k)) { |             if (parentRepo.contains(k)) { | ||||||
|                 return@withWriteLock |                 return@withWriteLock | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -1,24 +1,23 @@ | |||||||
| package dev.inmo.micro_utils.repos.cache.full.direct | package dev.inmo.micro_utils.repos.cache.full.direct | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* | import dev.inmo.micro_utils.common.Warning | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock |  | ||||||
| import dev.inmo.micro_utils.pagination.Pagination | import dev.inmo.micro_utils.pagination.Pagination | ||||||
| import dev.inmo.micro_utils.pagination.PaginationResult | import dev.inmo.micro_utils.pagination.PaginationResult | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.* | import dev.inmo.micro_utils.repos.cache.* | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode |  | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
|  |  | ||||||
| open class DirectFullReadCRUDCacheRepo<ObjectType, IdType>( | open class DirectFullReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>, |     protected val parentRepo: ReadCRUDRepo<ObjectType, IdType>, | ||||||
|     protected open val kvCache: KeyValueRepo<IdType, ObjectType>, |     protected val kvCache: KeyValueRepo<IdType, ObjectType>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     protected open val idGetter: (ObjectType) -> IdType |     protected val idGetter: (ObjectType) -> IdType | ||||||
| ) : ReadCRUDRepo<ObjectType, IdType>, DirectFullCacheRepo { | ) : ReadCRUDRepo<ObjectType, IdType>, DirectFullCacheRepo { | ||||||
|     protected open suspend fun actualizeAll() { |     protected open suspend fun actualizeAll() { | ||||||
|         kvCache.actualizeAll(parentRepo, locker = locker) |         kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
| @@ -60,20 +59,20 @@ fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.directlyCached( | |||||||
| ) = DirectFullReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ) = DirectFullReadCRUDCacheRepo(this, kvCache, locker, idGetter) | ||||||
|  |  | ||||||
| open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | ||||||
|     override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>, |     protected val crudRepo: CRUDRepo<ObjectType, IdType, InputValueType>, | ||||||
|     kvCache: KeyValueRepo<IdType, ObjectType>, |     kvCache: KeyValueRepo<IdType, ObjectType>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
|     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), |     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), | ||||||
|     idGetter: (ObjectType) -> IdType |     idGetter: (ObjectType) -> IdType | ||||||
| ) : DirectFullReadCRUDCacheRepo<ObjectType, IdType>( | ) : DirectFullReadCRUDCacheRepo<ObjectType, IdType>( | ||||||
|     parentRepo, |     crudRepo, | ||||||
|     kvCache, |     kvCache, | ||||||
|     locker, |     locker, | ||||||
|     idGetter |     idGetter | ||||||
| ), | ), | ||||||
|     WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( |     WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo( | ||||||
|         parentRepo, |         crudRepo, | ||||||
|         kvCache, |         kvCache, | ||||||
|         scope, |         scope, | ||||||
|         locker, |         locker, | ||||||
| @@ -82,7 +81,7 @@ open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|     CRUDRepo<ObjectType, IdType, InputValueType> { |     CRUDRepo<ObjectType, IdType, InputValueType> { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -99,6 +98,8 @@ open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| package dev.inmo.micro_utils.repos.cache.full.direct | package dev.inmo.micro_utils.repos.cache.full.direct | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.Pagination | import dev.inmo.micro_utils.pagination.Pagination | ||||||
| import dev.inmo.micro_utils.pagination.PaginationResult | import dev.inmo.micro_utils.pagination.PaginationResult | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo | import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo | ||||||
| import dev.inmo.micro_utils.repos.cache.full.FullReadKeyValueCacheRepo | import dev.inmo.micro_utils.repos.cache.full.FullReadKeyValueCacheRepo | ||||||
| import dev.inmo.micro_utils.repos.cache.full.FullWriteKeyValueCacheRepo | import dev.inmo.micro_utils.repos.cache.full.FullWriteKeyValueCacheRepo | ||||||
| @@ -18,8 +19,8 @@ import kotlinx.coroutines.flow.launchIn | |||||||
| import kotlinx.coroutines.flow.onEach | import kotlinx.coroutines.flow.onEach | ||||||
|  |  | ||||||
| open class DirectFullReadKeyValueCacheRepo<Key, Value>( | open class DirectFullReadKeyValueCacheRepo<Key, Value>( | ||||||
|     protected open val parentRepo: ReadKeyValueRepo<Key, Value>, |     protected val parentRepo: ReadKeyValueRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, Value>, |     protected val kvCache: KeyValueRepo<Key, Value>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker() |     protected val locker: SmartRWLocker = SmartRWLocker() | ||||||
| ) : DirectFullCacheRepo, ReadKeyValueRepo<Key, Value> { | ) : DirectFullCacheRepo, ReadKeyValueRepo<Key, Value> { | ||||||
|     protected open suspend fun actualizeAll() { |     protected open suspend fun actualizeAll() { | ||||||
| @@ -54,6 +55,7 @@ open class DirectFullReadKeyValueCacheRepo<Key, Value>( | |||||||
|         kvCache.keys(v, pagination, reversed) |         kvCache.keys(v, pagination, reversed) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
| @@ -65,8 +67,8 @@ fun <Key, Value> ReadKeyValueRepo<Key, Value>.directlyCached( | |||||||
| ) = DirectFullReadKeyValueCacheRepo(this, kvCache, locker) | ) = DirectFullReadKeyValueCacheRepo(this, kvCache, locker) | ||||||
|  |  | ||||||
| open class DirectFullWriteKeyValueCacheRepo<Key, Value>( | open class DirectFullWriteKeyValueCacheRepo<Key, Value>( | ||||||
|     protected open val parentRepo: WriteKeyValueRepo<Key, Value>, |     protected val parentRepo: WriteKeyValueRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, Value>, |     protected val kvCache: KeyValueRepo<Key, Value>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
| ) : DirectFullCacheRepo, WriteKeyValueRepo<Key, Value> by parentRepo { | ) : DirectFullCacheRepo, WriteKeyValueRepo<Key, Value> by parentRepo { | ||||||
| @@ -86,6 +88,7 @@ open class DirectFullWriteKeyValueCacheRepo<Key, Value>( | |||||||
|         } |         } | ||||||
|     }.launchIn(scope) |     }.launchIn(scope) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             kvCache.clear() |             kvCache.clear() | ||||||
| @@ -101,7 +104,7 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.directlyCached( | |||||||
| ) = DirectFullWriteKeyValueCacheRepo(this, kvCache, scope = scope) | ) = DirectFullWriteKeyValueCacheRepo(this, kvCache, scope = scope) | ||||||
|  |  | ||||||
| open class DirectFullKeyValueCacheRepo<Key, Value>( | open class DirectFullKeyValueCacheRepo<Key, Value>( | ||||||
|     override val parentRepo: KeyValueRepo<Key, Value>, |     protected val kvRepo: KeyValueRepo<Key, Value>, | ||||||
|     kvCache: KeyValueRepo<Key, Value>, |     kvCache: KeyValueRepo<Key, Value>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
| @@ -109,15 +112,15 @@ open class DirectFullKeyValueCacheRepo<Key, Value>( | |||||||
| ) : DirectFullCacheRepo, | ) : DirectFullCacheRepo, | ||||||
|     KeyValueRepo<Key, Value> , |     KeyValueRepo<Key, Value> , | ||||||
|     WriteKeyValueRepo<Key, Value> by DirectFullWriteKeyValueCacheRepo( |     WriteKeyValueRepo<Key, Value> by DirectFullWriteKeyValueCacheRepo( | ||||||
|         parentRepo, |         kvRepo, | ||||||
|         kvCache, |         kvCache, | ||||||
|         locker, |         locker, | ||||||
|         scope |         scope | ||||||
|     ), |     ), | ||||||
|     DirectFullReadKeyValueCacheRepo<Key, Value>(parentRepo, kvCache, locker) { |     DirectFullReadKeyValueCacheRepo<Key, Value>(kvRepo, kvCache, locker) { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -135,20 +138,21 @@ open class DirectFullKeyValueCacheRepo<Key, Value>( | |||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.actualizeAll(parentRepo, locker) |         kvCache.actualizeAll(parentRepo, locker) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun clear() { |     override suspend fun clear() { | ||||||
|         parentRepo.clear() |         kvRepo.clear() | ||||||
|         kvCache.clear() |         kvCache.clear() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset) |     override suspend fun unsetWithValues(toUnset: List<Value>) = kvRepo.unsetWithValues(toUnset) | ||||||
|  |  | ||||||
|     override suspend fun set(toSet: Map<Key, Value>) { |     override suspend fun set(toSet: Map<Key, Value>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.set(toSet) |             kvRepo.set(toSet) | ||||||
|             kvCache.set( |             kvCache.set( | ||||||
|                 toSet.filter { |                 toSet.filter { | ||||||
|                     parentRepo.contains(it.key) |                     parentRepo.contains(it.key) | ||||||
| @@ -159,7 +163,7 @@ open class DirectFullKeyValueCacheRepo<Key, Value>( | |||||||
|  |  | ||||||
|     override suspend fun unset(toUnset: List<Key>) { |     override suspend fun unset(toUnset: List<Key>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.unset(toUnset) |             kvRepo.unset(toUnset) | ||||||
|             kvCache.unset( |             kvCache.unset( | ||||||
|                 toUnset.filter { |                 toUnset.filter { | ||||||
|                     !parentRepo.contains(it) |                     !parentRepo.contains(it) | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ package dev.inmo.micro_utils.repos.cache.full.direct | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* | import dev.inmo.micro_utils.common.* | ||||||
| import dev.inmo.micro_utils.coroutines.SmartRWLocker | import dev.inmo.micro_utils.coroutines.SmartRWLocker | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions | ||||||
| import dev.inmo.micro_utils.coroutines.withReadAcquire | import dev.inmo.micro_utils.coroutines.withReadAcquire | ||||||
| import dev.inmo.micro_utils.coroutines.withWriteLock | import dev.inmo.micro_utils.coroutines.withWriteLock | ||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.pagination.utils.* | import dev.inmo.micro_utils.pagination.utils.* | ||||||
| import dev.inmo.micro_utils.repos.* | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation | ||||||
| import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode | ||||||
| import dev.inmo.micro_utils.repos.cache.util.actualizeAll | import dev.inmo.micro_utils.repos.cache.util.actualizeAll | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -15,8 +16,8 @@ import kotlinx.coroutines.Dispatchers | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| open class DirectFullReadKeyValuesCacheRepo<Key,Value>( | open class DirectFullReadKeyValuesCacheRepo<Key,Value>( | ||||||
|     protected open val parentRepo: ReadKeyValuesRepo<Key, Value>, |     protected val parentRepo: ReadKeyValuesRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, List<Value>>, |     protected val kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : ReadKeyValuesRepo<Key, Value>, DirectFullCacheRepo { | ) : ReadKeyValuesRepo<Key, Value>, DirectFullCacheRepo { | ||||||
|     protected open suspend fun actualizeKey(k: Key) { |     protected open suspend fun actualizeKey(k: Key) { | ||||||
| @@ -88,6 +89,7 @@ open class DirectFullReadKeyValuesCacheRepo<Key,Value>( | |||||||
|         return result ?: emptyPaginationResult() |         return result ?: emptyPaginationResult() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         actualizeAll() |         actualizeAll() | ||||||
|     } |     } | ||||||
| @@ -100,7 +102,7 @@ fun <Key, Value> ReadKeyValuesRepo<Key, Value>.directlyCached( | |||||||
|  |  | ||||||
| open class DirectFullWriteKeyValuesCacheRepo<Key,Value>( | open class DirectFullWriteKeyValuesCacheRepo<Key,Value>( | ||||||
|     parentRepo: WriteKeyValuesRepo<Key, Value>, |     parentRepo: WriteKeyValuesRepo<Key, Value>, | ||||||
|     protected open val kvCache: KeyValueRepo<Key, List<Value>>, |     protected val kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     protected val locker: SmartRWLocker = SmartRWLocker(), |     protected val locker: SmartRWLocker = SmartRWLocker(), | ||||||
| ) : WriteKeyValuesRepo<Key, Value> by parentRepo, DirectFullCacheRepo { | ) : WriteKeyValuesRepo<Key, Value> by parentRepo, DirectFullCacheRepo { | ||||||
| @@ -121,6 +123,7 @@ open class DirectFullWriteKeyValuesCacheRepo<Key,Value>( | |||||||
|         } |         } | ||||||
|     }.launchIn(scope) |     }.launchIn(scope) | ||||||
|  |  | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             kvCache.clear() |             kvCache.clear() | ||||||
| @@ -135,17 +138,17 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.directlyCached( | |||||||
| ) = DirectFullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) | ) = DirectFullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) | ||||||
|  |  | ||||||
| open class DirectFullKeyValuesCacheRepo<Key,Value>( | open class DirectFullKeyValuesCacheRepo<Key,Value>( | ||||||
|     override val parentRepo: KeyValuesRepo<Key, Value>, |     protected val kvsRepo: KeyValuesRepo<Key, Value>, | ||||||
|     kvCache: KeyValueRepo<Key, List<Value>>, |     kvCache: KeyValueRepo<Key, List<Value>>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     skipStartInvalidate: Boolean = false, |     skipStartInvalidate: Boolean = false, | ||||||
|     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), |     locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), | ||||||
| ) : KeyValuesRepo<Key, Value>, | ) : KeyValuesRepo<Key, Value>, | ||||||
|     DirectFullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker), |     DirectFullReadKeyValuesCacheRepo<Key, Value>(kvsRepo, kvCache, locker), | ||||||
|     WriteKeyValuesRepo<Key, Value> by parentRepo { |     WriteKeyValuesRepo<Key, Value> by kvsRepo { | ||||||
|     init { |     init { | ||||||
|         if (!skipStartInvalidate) { |         if (!skipStartInvalidate) { | ||||||
|             scope.launchSafelyWithoutExceptions { |             scope.launchLoggingDropExceptions { | ||||||
|                 if (locker.writeMutex.isLocked) { |                 if (locker.writeMutex.isLocked) { | ||||||
|                     initialInvalidate() |                     initialInvalidate() | ||||||
|                 } else { |                 } else { | ||||||
| @@ -170,13 +173,14 @@ open class DirectFullKeyValuesCacheRepo<Key,Value>( | |||||||
|             locker.unlockWrite() |             locker.unlockWrite() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     @OverrideRequireManualInvalidation | ||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.actualizeAll(parentRepo, locker = locker) |         kvCache.actualizeAll(parentRepo, locker = locker) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun set(toSet: Map<Key, List<Value>>) { |     override suspend fun set(toSet: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.set(toSet) |             kvsRepo.set(toSet) | ||||||
|             kvCache.set( |             kvCache.set( | ||||||
|                 toSet.filter { |                 toSet.filter { | ||||||
|                     parentRepo.contains(it.key) |                     parentRepo.contains(it.key) | ||||||
| @@ -187,7 +191,7 @@ open class DirectFullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun add(toAdd: Map<Key, List<Value>>) { |     override suspend fun add(toAdd: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.add(toAdd) |             kvsRepo.add(toAdd) | ||||||
|             toAdd.forEach { |             toAdd.forEach { | ||||||
|                 val filtered = it.value.filter { v -> |                 val filtered = it.value.filter { v -> | ||||||
|                     parentRepo.contains(it.key, v) |                     parentRepo.contains(it.key, v) | ||||||
| @@ -204,7 +208,7 @@ open class DirectFullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun remove(toRemove: Map<Key, List<Value>>) { |     override suspend fun remove(toRemove: Map<Key, List<Value>>) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.remove(toRemove) |             kvsRepo.remove(toRemove) | ||||||
|             toRemove.forEach { |             toRemove.forEach { | ||||||
|                 val filtered = it.value.filter { v -> |                 val filtered = it.value.filter { v -> | ||||||
|                     !parentRepo.contains(it.key, v) |                     !parentRepo.contains(it.key, v) | ||||||
| @@ -226,7 +230,7 @@ open class DirectFullKeyValuesCacheRepo<Key,Value>( | |||||||
|  |  | ||||||
|     override suspend fun clear(k: Key) { |     override suspend fun clear(k: Key) { | ||||||
|         locker.withWriteLock { |         locker.withWriteLock { | ||||||
|             parentRepo.clear(k) |             kvsRepo.clear(k) | ||||||
|             if (parentRepo.contains(k)) { |             if (parentRepo.contains(k)) { | ||||||
|                 return@withWriteLock |                 return@withWriteLock | ||||||
|             } |             } | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								repos/cache/src/jvmMain/kotlin/InvalidateSynchronously.kt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								repos/cache/src/jvmMain/kotlin/InvalidateSynchronously.kt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.repos.cache | ||||||
|  |  | ||||||
|  | import dev.inmo.kslog.common.KSLog | ||||||
|  | import dev.inmo.micro_utils.coroutines.doSynchronously | ||||||
|  | import dev.inmo.micro_utils.coroutines.runCatchingLogging | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  |  | ||||||
|  | fun <T : InvalidatableRepo> T.alsoInvalidateSync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     onFailure: suspend (Throwable) -> Unit = {}, | ||||||
|  | ) = also { | ||||||
|  |     scope.doSynchronously { | ||||||
|  |         runCatching { | ||||||
|  |             invalidate() | ||||||
|  |         }.onFailure { | ||||||
|  |             onFailure(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T : InvalidatableRepo> T.alsoInvalidateSync( | ||||||
|  |     onFailure: suspend (Throwable) -> Unit = {}, | ||||||
|  | ) = also { | ||||||
|  |     doSynchronously { | ||||||
|  |         runCatching { | ||||||
|  |             invalidate() | ||||||
|  |         }.onFailure { | ||||||
|  |             onFailure(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T : InvalidatableRepo> T.alsoInvalidateSyncLogging( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  | ) = also { | ||||||
|  |     scope.doSynchronously { | ||||||
|  |         runCatchingLogging(errorMessageBuilder, logger) { | ||||||
|  |             invalidate() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T : InvalidatableRepo> T.alsoInvalidateSyncLogging( | ||||||
|  |     errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, | ||||||
|  |     logger: KSLog = KSLog, | ||||||
|  | ) = also { | ||||||
|  |     doSynchronously { | ||||||
|  |         runCatchingLogging(errorMessageBuilder, logger) { | ||||||
|  |             invalidate() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -137,7 +137,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | |||||||
|     override suspend fun deleteById(ids: List<IdType>) { |     override suspend fun deleteById(ids: List<IdType>) { | ||||||
|         onBeforeDelete(ids) |         onBeforeDelete(ids) | ||||||
|         transaction(db = database) { |         transaction(db = database) { | ||||||
|             val deleted = deleteWhere(null, null) { selectByIds(it, ids) } |             val deleted = deleteWhere { selectByIds(it, ids) } | ||||||
|             if (deleted == ids.size) { |             if (deleted == ids.size) { | ||||||
|                 ids |                 ids | ||||||
|             } else { |             } else { | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo | |||||||
| import kotlinx.coroutines.channels.BufferOverflow | import kotlinx.coroutines.channels.BufferOverflow | ||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
| import org.jetbrains.exposed.sql.* | import org.jetbrains.exposed.sql.* | ||||||
| import org.jetbrains.exposed.sql.statements.InsertStatement |  | ||||||
| import org.jetbrains.exposed.sql.statements.UpdateBuilder | import org.jetbrains.exposed.sql.statements.UpdateBuilder | ||||||
| import org.jetbrains.exposed.sql.transactions.transaction | import org.jetbrains.exposed.sql.transactions.transaction | ||||||
|  |  | ||||||
| @@ -57,18 +56,48 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>( | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         transaction(database) { |         val (oldObjects, insertedResults) = transaction(database) { | ||||||
|  |             val oldObjects = selectAll().where { selectByIds(toSet.keys.toList()) }.map { it.asKey to it.asObject } | ||||||
|  |  | ||||||
|             deleteWhere { |             deleteWhere { | ||||||
|                 selectByIds(it, toSet.keys.toList()) |                 selectByIds(it, toSet.keys.toList()) | ||||||
|             } |             } | ||||||
|             batchInsert( |             val inserted = batchInsert( | ||||||
|                 prepreparedData, |                 prepreparedData, | ||||||
|             ) { (k, v) -> |             ) { (k, v) -> | ||||||
|                 insert(k, v, this) |                 insert(k, v, this) | ||||||
|             }.map { |             }.map { | ||||||
|                 it.asKey to it.asObject |                 it.asKey to it.asObject | ||||||
|             } |             } | ||||||
|         }.forEach { _onNewValue.emit(it) } |             oldObjects to inserted | ||||||
|  |         }.let { | ||||||
|  |             val mappedFirst = it | ||||||
|  |                 .first | ||||||
|  |                 .asSequence() | ||||||
|  |                 .groupBy { it.first } | ||||||
|  |                 .mapValues { it.value.map { it.second }.toSet() } | ||||||
|  |             val mappedSecond = it | ||||||
|  |                 .second | ||||||
|  |                 .asSequence() | ||||||
|  |                 .groupBy { it.first } | ||||||
|  |                 .mapValues { it.value.map { it.second }.toSet() } | ||||||
|  |             mappedFirst to mappedSecond | ||||||
|  |         } | ||||||
|  |         val deletedResults = oldObjects.mapNotNull { (k, vs) -> | ||||||
|  |             k to vs.filter { v -> | ||||||
|  |                 insertedResults[k] ?.contains(v) != true | ||||||
|  |             }.ifEmpty { return@mapNotNull null } | ||||||
|  |         } | ||||||
|  |         deletedResults.forEach { (k, vs) -> | ||||||
|  |             vs.forEach { v -> | ||||||
|  |                 _onValueRemoved.emit(k to v) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         insertedResults.forEach { (k, vs) -> | ||||||
|  |             vs.forEach { v -> | ||||||
|  |                 _onNewValue.emit(k to v) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun remove(toRemove: Map<Key, List<Value>>) { |     override suspend fun remove(toRemove: Map<Key, List<Value>>) { | ||||||
|   | |||||||
| @@ -73,7 +73,7 @@ class KtorCRUDRepoTests : CommonCRUDRepoTests() { | |||||||
|             } |             } | ||||||
|             val server = io.ktor.server.engine.embeddedServer( |             val server = io.ktor.server.engine.embeddedServer( | ||||||
|                 CIO, |                 CIO, | ||||||
|                 34567, |                 34568, | ||||||
|                 "127.0.0.1" |                 "127.0.0.1" | ||||||
|             ) { |             ) { | ||||||
|                 install(ContentNegotiation) { |                 install(ContentNegotiation) { | ||||||
| @@ -100,7 +100,7 @@ class KtorCRUDRepoTests : CommonCRUDRepoTests() { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>( |             val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>( | ||||||
|                 "http://127.0.0.1:34567", |                 "http://127.0.0.1:34568", | ||||||
|                 client, |                 client, | ||||||
|                 ContentType.Application.Json |                 ContentType.Application.Json | ||||||
|             ) { |             ) { | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ class KtorKeyValueRepoTests : CommonKeyValueRepoTests() { | |||||||
|             val repo = MapKeyValueRepo<Int, ComplexData>(map) |             val repo = MapKeyValueRepo<Int, ComplexData>(map) | ||||||
|             val server = io.ktor.server.engine.embeddedServer( |             val server = io.ktor.server.engine.embeddedServer( | ||||||
|                 CIO, |                 CIO, | ||||||
|                 34567, |                 34569, | ||||||
|                 "127.0.0.1" |                 "127.0.0.1" | ||||||
|             ) { |             ) { | ||||||
|                 install(ContentNegotiation) { |                 install(ContentNegotiation) { | ||||||
| @@ -91,7 +91,7 @@ class KtorKeyValueRepoTests : CommonKeyValueRepoTests() { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             val crudClient = KtorKeyValueRepoClient<Int, ComplexData>( |             val crudClient = KtorKeyValueRepoClient<Int, ComplexData>( | ||||||
|                 "http://127.0.0.1:34567", |                 "http://127.0.0.1:34569", | ||||||
|                 client, |                 client, | ||||||
|                 ContentType.Application.Json, |                 ContentType.Application.Json, | ||||||
|                 Int.serializer(), |                 Int.serializer(), | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ String[] includes = [ | |||||||
|     ":koin:generator:test", |     ":koin:generator:test", | ||||||
|     ":selector:common", |     ":selector:common", | ||||||
|     ":pagination:common", |     ":pagination:common", | ||||||
|  |     ":pagination:compose", | ||||||
|     ":pagination:exposed", |     ":pagination:exposed", | ||||||
|     ":pagination:ktor:common", |     ":pagination:ktor:common", | ||||||
|     ":pagination:ktor:server", |     ":pagination:ktor:server", | ||||||
| @@ -62,6 +63,10 @@ String[] includes = [ | |||||||
|     ":ksp:classcasts:generator", |     ":ksp:classcasts:generator", | ||||||
|     ":ksp:classcasts:generator:test", |     ":ksp:classcasts:generator:test", | ||||||
|  |  | ||||||
|  |     ":ksp:variations", | ||||||
|  |     ":ksp:variations:generator", | ||||||
|  |     ":ksp:variations:generator:test", | ||||||
|  |  | ||||||
|     ":dokka" |     ":dokka" | ||||||
| ] | ] | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user