mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-11-04 06:00:22 +00:00 
			
		
		
		
	Compare commits
	
		
			191 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8166d4b99b | |||
| b61d2ae2eb | |||
| 4790fe0aea | |||
| bc37b11cee | |||
| 223fed910f | |||
| b85ab7b061 | |||
| 888dc299c9 | |||
| e113dc28ed | |||
| 31e55d2307 | |||
| e90645f248 | |||
| 4bb7ba2571 | |||
| 8d31c25bf8 | |||
| c7ee1c28b2 | |||
| 99b09c8b28 | |||
| a328c4425a | |||
| c0f61ca896 | |||
| 86e70c0961 | |||
| d87a3a039f | |||
| 6279a2c40a | |||
| f377ebea88 | |||
| 7373fef964 | |||
| 41ef86dbda | |||
| 7bc2b2336d | |||
| 0a615e6d78 | |||
| 8f928e16e1 | |||
| 72bc8da1e7 | |||
| 51e349a5db | |||
| 3cbb19ba2c | |||
| ae3a5bf45d | |||
| 9c667f4b78 | |||
| 21195e1bcb | |||
| 03117ac565 | |||
| d13fbdf176 | |||
| 7cecc0e0b6 | |||
| 203e781f5d | |||
| 3eb6cd77cd | |||
| 51855b2405 | |||
| 00cc26d874 | |||
| 02520636ad | |||
| 76813fae8e | |||
| 4693483c2b | |||
| 6dbd12df59 | |||
| 9e84dc5031 | |||
| 8f790360bc | |||
| 808375cea6 | |||
| d9c15db9de | |||
| cf2be8ed43 | |||
| 97339c9b1d | |||
| eb35903de9 | |||
| 43e88e00da | |||
| 1e51f9d77b | |||
| dad738357c | |||
| cb828ab3f2 | |||
| aefaf4a8cc | |||
| b29f37a251 | |||
| 88854020ac | |||
| 14ebe01fc6 | |||
| 771aed0f0f | |||
| 16c57fcd6a | |||
| 75397a7ccb | |||
| b05404f828 | |||
| edea942874 | |||
| 8a8f568b9a | |||
| 4dc8d30c52 | |||
| cb4e08e823 | |||
| c443bf4fa0 | |||
| a6982de822 | |||
| 4f1a663e75 | |||
| dab262d626 | |||
| 87a3f61ca6 | |||
| 506e937a68 | |||
| 5a037c76dd | |||
| 313f622f7e | |||
| 6cba1fe1a2 | |||
| fd2d0e80b7 | |||
| 96ab2e8aca | |||
| 0202988cae | |||
| d619d59947 | |||
| 85b3e48d18 | |||
| 7a9b7d98a1 | |||
| b212acfcaf | |||
| 3a45e5dc70 | |||
| 73190518d5 | |||
| 03f78180dc | |||
| 1c0b8cf842 | |||
| a1624ea2a9 | |||
| 23a050cf1e | |||
| 916f2f96f4 | |||
| 00cc214754 | |||
| b2e38f72b9 | |||
| e7107d238d | |||
| ed9ebdbd1a | |||
| e80676d3d2 | |||
| 02d02fa8f2 | |||
| bd783fb74f | |||
| 50386adf70 | |||
| f4ee6c2890 | |||
| d45aef9fe5 | |||
| a56cd3dddd | |||
| 419e7070ee | |||
| 612cf40b5f | |||
| 8b39882e83 | |||
| e639ae172b | |||
| d0446850ae | |||
| c48465b90b | |||
| f419fd03d2 | |||
| 494812a660 | |||
| eb78f21eec | |||
| 4bda70268b | |||
| f037ce4371 | |||
| 3d2196e35d | |||
| a74f061b02 | |||
| 11ade14676 | |||
| eb562d8784 | |||
| 1ee5b4bfd4 | |||
| d97892080b | |||
| 6f37125724 | |||
| ed1baaade7 | |||
| bb9669f8fd | |||
| bdac715d48 | |||
| acf4971298 | |||
| 249bc83a8c | |||
| 0fbb92f03f | |||
| ca27cb3f82 | |||
| 3a5771a0cc | |||
| 527a2a91ac | |||
| 6763e5c4c6 | |||
| 06918d8310 | |||
| 89ccaa1b57 | |||
| 5d0bdb9bcf | |||
| 31fdcf74a5 | |||
| afca09cc1d | |||
| 531d89d9db | |||
| 6bbbea0bc3 | |||
| e337cd98c8 | |||
| bcbab3b380 | |||
| fb63de7568 | |||
| aa45a4ab13 | |||
| 2af7e2f681 | |||
| 34fd9edce0 | |||
| 2a4cb8c5f9 | |||
| 50ea40bc3a | |||
| a77654052d | |||
| 88aafce552 | |||
| 4e95d6bfff | |||
| 38d0e34fb5 | |||
| 8fbc6b9041 | |||
| e8219d6cf4 | |||
| 6c20fc4ca6 | |||
| 85cd975492 | |||
| 1171a717fe | |||
| bbe5320312 | |||
| 00acb9fddd | |||
| de3d14dc41 | |||
| 67ff9cc9b3 | |||
| af132103a0 | |||
| 3b1124a804 | |||
| f226c2dfd6 | |||
| 69d6e63846 | |||
| 02c3d397ad | |||
| 67a1050646 | |||
| 8cd0775a6c | |||
| 162294d6c6 | |||
| c4dd19dd00 | |||
| d2314422f1 | |||
| 6fedd6f859 | |||
| e52b59665f | |||
| cda9d09689 | |||
| c9237b3f00 | |||
| 18bba66c4a | |||
| 63418c4a8a | |||
| 2e66c6f4e3 | |||
| e9c5df4c13 | |||
| bc7789ad2c | |||
| e3da761249 | |||
| 4082f65afa | |||
| 5d1cab075d | |||
| bcf67f7e59 | |||
| 7d3b1f8e75 | |||
| 119a0588cc | |||
| fab789d9c0 | |||
| ceba81c08f | |||
| a061af0558 | |||
| c7a53846ad | |||
| a683cccf0c | |||
| 50d41e35c1 | |||
| aa0e831cea | |||
| 44e26ccb4f | |||
| 2a783f6e2b | |||
| 6058d6a724 | |||
| 2e9c7eb5fa | 
							
								
								
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,10 +10,10 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-java@v1
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: 1.8
 | 
			
		||||
      - name: Fix android 31.0.0 dx
 | 
			
		||||
          java-version: 11
 | 
			
		||||
      - name: Fix android 32.0.0 dx
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: ./gradlew dokkaHtml
 | 
			
		||||
      - name: Publish KDocs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							@@ -8,10 +8,10 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-java@v1
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: 1.8
 | 
			
		||||
      - name: Fix android 31.0.0 dx
 | 
			
		||||
          java-version: 11
 | 
			
		||||
      - name: Fix android 32.0.0 dx
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
      - name: Rewrite version
 | 
			
		||||
        run: |
 | 
			
		||||
          branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
 | 
			
		||||
@@ -22,7 +22,7 @@ jobs:
 | 
			
		||||
        run: ./gradlew build
 | 
			
		||||
      - name: Publish
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
        run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
 | 
			
		||||
        run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUBPACKAGES_USER: ${{ github.actor }}
 | 
			
		||||
          GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -11,5 +11,6 @@ out/
 | 
			
		||||
 | 
			
		||||
secret.gradle
 | 
			
		||||
local.properties
 | 
			
		||||
kotlin-js-store
 | 
			
		||||
 | 
			
		||||
publishing.sh
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										324
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										324
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,9 +1,333 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 0.9.19
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Coroutines`: `1.6.0` -> `1.6.1`
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * Fixes in `ExposedStandardVersionsRepoProxy`
 | 
			
		||||
 | 
			
		||||
## 0.9.18
 | 
			
		||||
 | 
			
		||||
* `Common`
 | 
			
		||||
    * New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
 | 
			
		||||
 | 
			
		||||
## 0.9.17
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
    * New extension `Element.visibilityFlow()`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * Now it is possible to resolve conflicts on `startChain`
 | 
			
		||||
 | 
			
		||||
## 0.9.16
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.6.3` -> `2.7.0`
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * New extension `Node#onRemoved`
 | 
			
		||||
    * `Compose`:
 | 
			
		||||
        * New extension `Composition#linkWithRoot` for removing of composition with root element
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
    * `Compose`:
 | 
			
		||||
        * New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
 | 
			
		||||
 | 
			
		||||
## 0.9.15
 | 
			
		||||
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
 | 
			
		||||
    * `DefaultStatesManager` now is extendable
 | 
			
		||||
    * `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
 | 
			
		||||
 | 
			
		||||
## 0.9.14
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.6.2` -> `2.6.3`
 | 
			
		||||
    * `Ktor`: `1.6.7` -> `1.6.8`
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * Add temporal files uploading functionality (for clients to upload and for server to receive)
 | 
			
		||||
 | 
			
		||||
## 0.9.13
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Compose`: `1.1.0` -> `1.1.1`
 | 
			
		||||
 | 
			
		||||
## 0.9.12
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * `JS`:
 | 
			
		||||
        * New function `openLink`
 | 
			
		||||
        * New function `selectFile`
 | 
			
		||||
        * New function `triggerDownloadFile`
 | 
			
		||||
    * `Compose`:
 | 
			
		||||
        * Created :)
 | 
			
		||||
        * `Common`:
 | 
			
		||||
            * `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
 | 
			
		||||
        * `JS`:
 | 
			
		||||
            * `openLink` on top of `openLink` with `String` target from common
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
    * `Compose`:
 | 
			
		||||
        * `Common`:
 | 
			
		||||
            * New extension `Flow.toMutableState`
 | 
			
		||||
            * New extension `StateFlow.toMutableState`
 | 
			
		||||
        * `JS`:
 | 
			
		||||
            * New function `selectFileOrThrow` on top of `selectFile` from `common`
 | 
			
		||||
            * New function `selectFileOrNull` on top of `selectFile` from `common`
 | 
			
		||||
 | 
			
		||||
## 0.9.11
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.6.1` -> `2.6.2`
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
    * `Compose`:
 | 
			
		||||
        * Created :)
 | 
			
		||||
        * New extensions and function:
 | 
			
		||||
            * `Composition#linkWithJob`
 | 
			
		||||
            * `Composition#linkWithContext`
 | 
			
		||||
            * `renderComposableAndLinkToContext`
 | 
			
		||||
 | 
			
		||||
## 0.9.10
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.5.2` -> `2.6.1`
 | 
			
		||||
* Ktor:
 | 
			
		||||
    * Client:
 | 
			
		||||
        * New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
 | 
			
		||||
    * Server:
 | 
			
		||||
        * Now it is possible to filter data in `Route#includeWebsocketHandling`
 | 
			
		||||
        * Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
 | 
			
		||||
        * Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
 | 
			
		||||
 | 
			
		||||
## 0.9.9
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.5.1` -> `2.5.2`
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * Add new diff tool - `applyDiff`
 | 
			
		||||
    * Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
 | 
			
		||||
 | 
			
		||||
## 0.9.8
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Exposed`: `0.37.2` -> `0.37.3`
 | 
			
		||||
    * `Klock`: `2.4.13` -> `2.5.1`
 | 
			
		||||
    * `AppCompat`: `1.4.0` -> `1.4.1`
 | 
			
		||||
 | 
			
		||||
## 0.9.7
 | 
			
		||||
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Server`:
 | 
			
		||||
        * `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
 | 
			
		||||
 | 
			
		||||
## 0.9.6
 | 
			
		||||
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
 | 
			
		||||
 | 
			
		||||
## 0.9.5
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.12` -> `2.4.13`
 | 
			
		||||
 | 
			
		||||
## 0.9.4
 | 
			
		||||
 | 
			
		||||
* `Pagination`:
 | 
			
		||||
    * `Common`:
 | 
			
		||||
        * Add several `optionallyReverse` functions
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * Changes in `Either`:
 | 
			
		||||
        * Now `Either` uses `optionalT1` and `optionalT2` as main properties
 | 
			
		||||
        * `Either#t1` and `Either#t2` are deprecated
 | 
			
		||||
        * New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
 | 
			
		||||
 | 
			
		||||
## 0.9.3
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `UUID`: `0.3.1` -> `0.4.0`
 | 
			
		||||
 | 
			
		||||
## 0.9.2
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.10` -> `2.4.12`
 | 
			
		||||
 | 
			
		||||
## 0.9.1
 | 
			
		||||
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * Default realizations of standard interfaces for exposed DB are using public fields for now:
 | 
			
		||||
            * `ExposedReadKeyValueRepo`
 | 
			
		||||
            * `ExposedReadOneToManyKeyValueRepo`
 | 
			
		||||
            * `ExposedStandardVersionsRepoProxy`
 | 
			
		||||
        * New typealiases for one to many exposed realizations:
 | 
			
		||||
            * `ExposedReadKeyValuesRepo`
 | 
			
		||||
            * `ExposedKeyValuesRepo`
 | 
			
		||||
 | 
			
		||||
## 0.9.0
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Kotlin`: `1.5.31` -> `1.6.10`
 | 
			
		||||
    * `Coroutines`: `1.5.2` -> `1.6.0`
 | 
			
		||||
    * `Serialization`: `1.3.1` -> `1.3.2`
 | 
			
		||||
    * `Exposed`: `0.36.2` -> `0.37.2`
 | 
			
		||||
    * `Ktor`: `1.6.5` -> `1.6.7`
 | 
			
		||||
    * `Klock`: `2.4.8` -> `2.4.10`
 | 
			
		||||
 | 
			
		||||
## 0.8.9
 | 
			
		||||
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Server`:
 | 
			
		||||
        * Fixes in `uniloadMultipart`
 | 
			
		||||
    * `Client`:
 | 
			
		||||
        * Fixes in `unimultipart`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * Fixes in `DefaultUpdatableStatesMachine`
 | 
			
		||||
 | 
			
		||||
## 0.8.8
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `AppCompat`: `1.3.1` -> `1.4.0`
 | 
			
		||||
    * Android Compile SDK: `31.0.0` -> `32.0.0`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * `DefaultStatesMachine` now is extendable
 | 
			
		||||
    * New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
 | 
			
		||||
 | 
			
		||||
## 0.8.7
 | 
			
		||||
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Client`:
 | 
			
		||||
        * `UnifiedRequester` now have no private fields
 | 
			
		||||
        * Add preview work with multipart
 | 
			
		||||
    * `Server`
 | 
			
		||||
        * `UnifiedRouter` now have no private fields
 | 
			
		||||
        * Add preview work with multipart
 | 
			
		||||
 | 
			
		||||
## 0.8.6
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
 | 
			
		||||
    * All `joinTo` now accept not `crossinline` callbacks
 | 
			
		||||
 | 
			
		||||
## 0.8.5
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * `repeatOnFailure`
 | 
			
		||||
 | 
			
		||||
## 0.8.4
 | 
			
		||||
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Server`:
 | 
			
		||||
        * Several new `createKtorServer`
 | 
			
		||||
 | 
			
		||||
## 0.8.3
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * Ranges intersection functionality
 | 
			
		||||
    * New type `Optional`
 | 
			
		||||
* `Pagination`:
 | 
			
		||||
    * `Pagination` now extends `ClosedRange<Int>`
 | 
			
		||||
    * `Pagination` intersection functionality
 | 
			
		||||
 | 
			
		||||
## 0.8.2
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.7` -> `2.4.8`
 | 
			
		||||
    * `Serialization`: `1.3.0` -> `1.3.1`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
 | 
			
		||||
    * Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
 | 
			
		||||
 | 
			
		||||
## 0.8.1
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Exposed`: `0.36.1` -> `0.36.2`
 | 
			
		||||
    * `Core KTX`: `1.6.0` -> `1.7.0`
 | 
			
		||||
 | 
			
		||||
## 0.8.0
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.6` -> `2.4.7`
 | 
			
		||||
    * `Ktor`: `1.6.4` -> `1.6.5`
 | 
			
		||||
    * `Exposed`: `0.35.3` -> `0.36.1`
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * Type `Either` got its own serializer
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * `Common`:
 | 
			
		||||
        * Full rework of FSM:
 | 
			
		||||
            * Now it is more flexible for checking of handler opportunity to handle state
 | 
			
		||||
            * Now machine and states managers are type-oriented
 | 
			
		||||
            * `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
 | 
			
		||||
        * Add opportunity for comfortable adding default state handler
 | 
			
		||||
 | 
			
		||||
## 0.7.4
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * New type `Either`
 | 
			
		||||
* `Serialization`:
 | 
			
		||||
    * `TypedSerializer`
 | 
			
		||||
        * New factory fun which accept vararg pairs of type and its serializer
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Common` (`Android`):
 | 
			
		||||
        * `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
 | 
			
		||||
          android crud repo _WILL NOT_ send previous events to the 
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
 | 
			
		||||
        * KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
 | 
			
		||||
          inheritors
 | 
			
		||||
* `Pagination`:
 | 
			
		||||
    * `Common`:
 | 
			
		||||
        * New types `getAllBy*` for current, next and custom paging
 | 
			
		||||
 | 
			
		||||
## 0.7.3
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Exposed`: `0.35.2` -> `0.35.3`
 | 
			
		||||
 | 
			
		||||
## 0.7.2
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.5` -> `2.4.6`
 | 
			
		||||
 | 
			
		||||
## 0.7.1
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.3` -> `2.4.5`
 | 
			
		||||
    * `Exposed`: `0.35.1` -> `0.35.2`
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
    * `Common`:
 | 
			
		||||
        * New `Flow` - `AccumulatorFlow`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * `Common`:
 | 
			
		||||
        * `InMemoryStatesManager` has been replaced
 | 
			
		||||
        * `StatesMachine` became an interface
 | 
			
		||||
        * New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of
 | 
			
		||||
          data info
 | 
			
		||||
 | 
			
		||||
## 0.7.0
 | 
			
		||||
 | 
			
		||||
**THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL**
 | 
			
		||||
 | 
			
		||||
* `Versions`
 | 
			
		||||
    * `kotlinx.datetime` -> `Klock`
 | 
			
		||||
 | 
			
		||||
## 0.6.0 DO NOT RECOMMENDED
 | 
			
		||||
 | 
			
		||||
**THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL**
 | 
			
		||||
**ALL DEPRECATION HAVE BEEN REMOVED**
 | 
			
		||||
 | 
			
		||||
* `Versions`
 | 
			
		||||
    * `Klock` -> `kotlinx.datetime`
 | 
			
		||||
 | 
			
		||||
## 0.5.31
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.2` -> `2.4.3`
 | 
			
		||||
    * `Ktor`: `1.6.3` -> `1.6.4`
 | 
			
		||||
 | 
			
		||||
## 0.5.30
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        androidMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "androidx.appcompat:appcompat-resources:$appcompat_version"
 | 
			
		||||
                api libs.android.appCompat.resources
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,13 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
 | 
			
		||||
                api libs.kt.coroutines
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        androidMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
 | 
			
		||||
                api libs.android.recyclerView
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							@@ -7,12 +7,12 @@ buildscript {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:4.1.3'
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
 | 
			
		||||
        classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
 | 
			
		||||
        classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
 | 
			
		||||
        classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
 | 
			
		||||
        classpath libs.buildscript.kt.gradle
 | 
			
		||||
        classpath libs.buildscript.kt.serialization
 | 
			
		||||
        classpath libs.buildscript.jb.dokka
 | 
			
		||||
        classpath libs.buildscript.gh.release
 | 
			
		||||
        classpath libs.buildscript.android.gradle
 | 
			
		||||
        classpath libs.buildscript.android.dexcount
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    id "org.jetbrains.kotlin.multiplatform"
 | 
			
		||||
    id "org.jetbrains.kotlin.plugin.serialization"
 | 
			
		||||
    id "com.android.library"
 | 
			
		||||
    alias(libs.plugins.jb.compose)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
package dev.inmo.micro_utils.common.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.DisposableEffectResult
 | 
			
		||||
 | 
			
		||||
class DefaultDisposableEffectResult(
 | 
			
		||||
    private val onDispose: () -> Unit
 | 
			
		||||
) : DisposableEffectResult {
 | 
			
		||||
    override fun dispose() {
 | 
			
		||||
        onDispose()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val DoNothing = DefaultDisposableEffectResult {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package dev.inmo.micro_utils.common.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Composition
 | 
			
		||||
import dev.inmo.micro_utils.common.onRemoved
 | 
			
		||||
import org.w3c.dom.Element
 | 
			
		||||
 | 
			
		||||
fun Composition.linkWithElement(element: Element) {
 | 
			
		||||
    element.onRemoved { dispose() }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package dev.inmo.micro_utils.common.compose
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.compose.web.attributes.ATarget
 | 
			
		||||
 | 
			
		||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
 | 
			
		||||
    link,
 | 
			
		||||
    mode.targetStr,
 | 
			
		||||
    features
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
package dev.inmo.micro_utils.common.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import org.jetbrains.compose.web.dom.DOMScope
 | 
			
		||||
import org.w3c.dom.Element
 | 
			
		||||
 | 
			
		||||
fun <TElement : Element> renderComposableAndLinkToRoot(
 | 
			
		||||
    root: TElement,
 | 
			
		||||
    monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
 | 
			
		||||
    content: @Composable DOMScope<TElement>.() -> Unit
 | 
			
		||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
 | 
			
		||||
    linkWithElement(root)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
@Deprecated("Redundant", ReplaceWith("coerceIn(min, max)"))
 | 
			
		||||
@Suppress("NOTHING_TO_INLINE")
 | 
			
		||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max)
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlin.jvm.JvmInline
 | 
			
		||||
 | 
			
		||||
private inline fun <T> getObject(
 | 
			
		||||
    additional: MutableList<T>,
 | 
			
		||||
    iterator: Iterator<T>
 | 
			
		||||
@@ -155,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
 | 
			
		||||
inline fun <T> Iterable<T>.calculateStrictDiff(
 | 
			
		||||
    other: Iterable<T>
 | 
			
		||||
) = calculateDiff(other, strictComparison = true)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
 | 
			
		||||
 * mutable list
 | 
			
		||||
 */
 | 
			
		||||
fun <T> MutableList<T>.applyDiff(
 | 
			
		||||
    source: Iterable<T>,
 | 
			
		||||
    strictComparison: Boolean = false
 | 
			
		||||
) = calculateDiff(source, strictComparison).let {
 | 
			
		||||
    for (i in it.removed.indices.sortedDescending()) {
 | 
			
		||||
        removeAt(it.removed[i].index)
 | 
			
		||||
    }
 | 
			
		||||
    it.added.forEach { (i, t) ->
 | 
			
		||||
        add(i, t)
 | 
			
		||||
    }
 | 
			
		||||
    it.replaced.forEach { (_, new) ->
 | 
			
		||||
        set(new.index, new.value)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,166 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.*
 | 
			
		||||
import kotlinx.serialization.builtins.serializer
 | 
			
		||||
import kotlinx.serialization.descriptors.*
 | 
			
		||||
import kotlinx.serialization.encoding.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
 | 
			
		||||
 *
 | 
			
		||||
 * @see EitherFirst
 | 
			
		||||
 * @see EitherSecond
 | 
			
		||||
 * @see Either.Companion.first
 | 
			
		||||
 * @see Either.Companion.second
 | 
			
		||||
 * @see Either.onFirst
 | 
			
		||||
 * @see Either.onSecond
 | 
			
		||||
 * @see Either.mapOnFirst
 | 
			
		||||
 * @see Either.mapOnSecond
 | 
			
		||||
 */
 | 
			
		||||
@Serializable(EitherSerializer::class)
 | 
			
		||||
sealed interface Either<T1, T2> {
 | 
			
		||||
    val optionalT1: Optional<T1>
 | 
			
		||||
    val optionalT2: Optional<T2>
 | 
			
		||||
    @Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
 | 
			
		||||
    val t1: T1?
 | 
			
		||||
        get() = optionalT1.dataOrNull()
 | 
			
		||||
    @Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
 | 
			
		||||
    val t2: T2?
 | 
			
		||||
        get() = optionalT2.dataOrNull()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <T1, T2> serializer(
 | 
			
		||||
            t1Serializer: KSerializer<T1>,
 | 
			
		||||
            t2Serializer: KSerializer<T2>,
 | 
			
		||||
        ): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class EitherSerializer<T1, T2>(
 | 
			
		||||
    t1Serializer: KSerializer<T1>,
 | 
			
		||||
    t2Serializer: KSerializer<T2>,
 | 
			
		||||
) : KSerializer<Either<T1, T2>> {
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override val descriptor: SerialDescriptor = buildSerialDescriptor(
 | 
			
		||||
        "TypedSerializer",
 | 
			
		||||
        SerialKind.CONTEXTUAL
 | 
			
		||||
    ) {
 | 
			
		||||
        element("type", String.serializer().descriptor)
 | 
			
		||||
        element("value", ContextualSerializer(Either::class).descriptor)
 | 
			
		||||
    }
 | 
			
		||||
    private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
 | 
			
		||||
    private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
 | 
			
		||||
 | 
			
		||||
    override fun deserialize(decoder: Decoder): Either<T1, T2> {
 | 
			
		||||
        return decoder.decodeStructure(descriptor) {
 | 
			
		||||
            var type: String? = null
 | 
			
		||||
            lateinit var result: Either<T1, T2>
 | 
			
		||||
            while (true) {
 | 
			
		||||
                when (val index = decodeElementIndex(descriptor)) {
 | 
			
		||||
                    0 -> type = decodeStringElement(descriptor, 0)
 | 
			
		||||
                    1 -> {
 | 
			
		||||
                        result = when (type) {
 | 
			
		||||
                            "t1" -> decodeSerializableElement(
 | 
			
		||||
                                descriptor,
 | 
			
		||||
                                1,
 | 
			
		||||
                                t1EitherSerializer
 | 
			
		||||
                            )
 | 
			
		||||
                            "t2" -> decodeSerializableElement(
 | 
			
		||||
                                descriptor,
 | 
			
		||||
                                1,
 | 
			
		||||
                                t2EitherSerializer
 | 
			
		||||
                            )
 | 
			
		||||
                            else -> error("Unknown type of either: $type")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    CompositeDecoder.DECODE_DONE -> break
 | 
			
		||||
                    else -> error("Unexpected index: $index")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            result
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
 | 
			
		||||
        encoder.encodeStructure(descriptor) {
 | 
			
		||||
            when (value) {
 | 
			
		||||
                is EitherFirst -> {
 | 
			
		||||
                    encodeStringElement(descriptor, 0, "t1")
 | 
			
		||||
                    encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
 | 
			
		||||
                }
 | 
			
		||||
                is EitherSecond -> {
 | 
			
		||||
                    encodeStringElement(descriptor, 0, "t2")
 | 
			
		||||
                    encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This type [Either] will always have not nullable [optionalT1]
 | 
			
		||||
 */
 | 
			
		||||
@Serializable
 | 
			
		||||
data class EitherFirst<T1, T2>(
 | 
			
		||||
    override val t1: T1
 | 
			
		||||
) : Either<T1, T2> {
 | 
			
		||||
    override val optionalT1: Optional<T1> = t1.optional
 | 
			
		||||
    override val optionalT2: Optional<T2> = Optional.absent()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This type [Either] will always have not nullable [optionalT2]
 | 
			
		||||
 */
 | 
			
		||||
@Serializable
 | 
			
		||||
data class EitherSecond<T1, T2>(
 | 
			
		||||
    override val t2: T2
 | 
			
		||||
) : Either<T1, T2> {
 | 
			
		||||
    override val optionalT1: Optional<T1> = Optional.absent()
 | 
			
		||||
    override val optionalT2: Optional<T2> = t2.optional
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @return New instance of [EitherFirst]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
 | 
			
		||||
/**
 | 
			
		||||
 * @return New instance of [EitherSecond]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] in case when [this] is [EitherFirst]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
 | 
			
		||||
    optionalT1.onPresented(block)
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] in case when [this] is [EitherSecond]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
 | 
			
		||||
    optionalT2.onPresented(block)
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @return Result of [block] if [this] is [EitherFirst]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
 | 
			
		||||
    return optionalT1.mapOnPresented(block)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @return Result of [block] if [this] is [EitherSecond]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
 | 
			
		||||
    return optionalT2.mapOnPresented(block)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <reified T1, reified T2> Any.either() = when (this) {
 | 
			
		||||
    is T1 -> Either.first<T1, T2>(this)
 | 
			
		||||
    is T2 -> Either.second<T1, T2>(this)
 | 
			
		||||
    else -> error("Incorrect type of either argument $this")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
inline fun <I, R> Iterable<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> R?,
 | 
			
		||||
    separatorFun: (I) -> R?,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): List<R> {
 | 
			
		||||
    val result = mutableListOf<R>()
 | 
			
		||||
    val iterator = iterator()
 | 
			
		||||
@@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo(
 | 
			
		||||
    separator: R? = null,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
 | 
			
		||||
 | 
			
		||||
inline fun <I> Iterable<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> I?,
 | 
			
		||||
    separatorFun: (I) -> I?,
 | 
			
		||||
    prefix: I? = null,
 | 
			
		||||
    postfix: I? = null
 | 
			
		||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
 | 
			
		||||
@@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo(
 | 
			
		||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
 | 
			
		||||
 | 
			
		||||
inline fun <I, reified R> Array<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> R?,
 | 
			
		||||
    separatorFun: (I) -> R?,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
 | 
			
		||||
 | 
			
		||||
inline fun <I, reified R> Array<I>.joinTo(
 | 
			
		||||
    separator: R? = null,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,12 @@ value class FileName(val string: String) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@PreviewFeature
 | 
			
		||||
expect class MPPFile
 | 
			
		||||
 | 
			
		||||
expect val MPPFile.filename: FileName
 | 
			
		||||
expect val MPPFile.filesize: Long
 | 
			
		||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
 | 
			
		||||
suspend fun MPPFile.bytes() = bytesAllocator()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
@file:Suppress("unused")
 | 
			
		||||
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
 | 
			
		||||
 * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
 | 
			
		||||
 * presented by some third type.
 | 
			
		||||
 *
 | 
			
		||||
 * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
 | 
			
		||||
 * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
 | 
			
		||||
 * cause this null will say nothing about availability of the row (of course, it is exaggerated example)
 | 
			
		||||
 *
 | 
			
		||||
 * @see Optional.presented
 | 
			
		||||
 * @see Optional.absent
 | 
			
		||||
 * @see Optional.optional
 | 
			
		||||
 * @see Optional.onPresented
 | 
			
		||||
 * @see Optional.onAbsent
 | 
			
		||||
 */
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Optional<T> internal constructor(
 | 
			
		||||
    @Warning("It is unsafe to use this data directly")
 | 
			
		||||
    val data: T?,
 | 
			
		||||
    @Warning("It is unsafe to use this data directly")
 | 
			
		||||
    val dataPresented: Boolean
 | 
			
		||||
) {
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Will create [Optional] with presented data
 | 
			
		||||
         */
 | 
			
		||||
        fun <T> presented(data: T) = Optional(data, true)
 | 
			
		||||
        /**
 | 
			
		||||
         * Will create [Optional] without data
 | 
			
		||||
         */
 | 
			
		||||
        fun <T> absent() = Optional<T>(null, false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline val <T> T.optional
 | 
			
		||||
    get() = Optional.presented(this)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
 | 
			
		||||
    if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
 | 
			
		||||
    if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data absent ([Optional.dataPresented] == false)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
 | 
			
		||||
    if (!dataPresented) { block() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
 | 
			
		||||
    if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
 | 
			
		||||
 */
 | 
			
		||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
 | 
			
		||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
 | 
			
		||||
    start == other.start && endInclusive == other.endInclusive -> start to endInclusive
 | 
			
		||||
    start > other.endInclusive || other.start > endInclusive -> null
 | 
			
		||||
    else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun IntRange.intersect(
 | 
			
		||||
    other: IntRange
 | 
			
		||||
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
 | 
			
		||||
    it.first .. it.second
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun LongRange.intersect(
 | 
			
		||||
    other: LongRange
 | 
			
		||||
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
 | 
			
		||||
    it.first .. it.second
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Executes the given [action] until getting of successful result specified number of [times].
 | 
			
		||||
 *
 | 
			
		||||
 * A zero-based index of current iteration is passed as a parameter to [action].
 | 
			
		||||
 */
 | 
			
		||||
inline fun <R> repeatOnFailure(
 | 
			
		||||
    times: Int,
 | 
			
		||||
    onEachFailure: (Throwable) -> Unit = {},
 | 
			
		||||
    action: (Int) -> R
 | 
			
		||||
): Optional<R> {
 | 
			
		||||
    repeat(times) {
 | 
			
		||||
        runCatching {
 | 
			
		||||
            action(it)
 | 
			
		||||
        }.onFailure(onEachFailure).onSuccess {
 | 
			
		||||
            return Optional.presented(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return Optional.absent()
 | 
			
		||||
}
 | 
			
		||||
@@ -54,7 +54,7 @@ class DiffUtilsTests {
 | 
			
		||||
        val oldList = (0 until 10).map { it.toString() }
 | 
			
		||||
        val withIndex = oldList.withIndex()
 | 
			
		||||
 | 
			
		||||
        for (step in 0 until oldList.size) {
 | 
			
		||||
        for (step in oldList.indices) {
 | 
			
		||||
            for ((i, v) in withIndex) {
 | 
			
		||||
                val mutable = oldList.toMutableList()
 | 
			
		||||
                val changes = (
 | 
			
		||||
@@ -73,4 +73,78 @@ class DiffUtilsTests {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testThatSimpleRemoveApplyWorks() {
 | 
			
		||||
        val oldList = (0 until 10).toList()
 | 
			
		||||
        val withIndex = oldList.withIndex()
 | 
			
		||||
 | 
			
		||||
        for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
 | 
			
		||||
            for ((i, _) in withIndex) {
 | 
			
		||||
                if (i + count > oldList.lastIndex) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                val removedSublist = oldList.subList(i, i + count)
 | 
			
		||||
                val mutableOldList = oldList.toMutableList()
 | 
			
		||||
                val targetList = oldList - removedSublist
 | 
			
		||||
 | 
			
		||||
                mutableOldList.applyDiff(targetList)
 | 
			
		||||
 | 
			
		||||
                assertEquals(
 | 
			
		||||
                    targetList,
 | 
			
		||||
                    mutableOldList
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testThatSimpleAddApplyWorks() {
 | 
			
		||||
        val oldList = (0 until 10).map { it.toString() }
 | 
			
		||||
        val withIndex = oldList.withIndex()
 | 
			
		||||
 | 
			
		||||
        for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
 | 
			
		||||
            for ((i, v) in withIndex) {
 | 
			
		||||
                if (i + count > oldList.lastIndex) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                val addedSublist = oldList.subList(i, i + count).map { "added$it" }
 | 
			
		||||
                val mutable = oldList.toMutableList()
 | 
			
		||||
                mutable.addAll(i, addedSublist)
 | 
			
		||||
                val mutableOldList = oldList.toMutableList()
 | 
			
		||||
 | 
			
		||||
                mutableOldList.applyDiff(mutable)
 | 
			
		||||
 | 
			
		||||
                assertEquals(
 | 
			
		||||
                    mutable,
 | 
			
		||||
                    mutableOldList
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testThatSimpleChangesApplyWorks() {
 | 
			
		||||
        val oldList = (0 until 10).map { it.toString() }
 | 
			
		||||
        val withIndex = oldList.withIndex()
 | 
			
		||||
 | 
			
		||||
        for (step in oldList.indices) {
 | 
			
		||||
            for ((i, v) in withIndex) {
 | 
			
		||||
                val mutable = oldList.toMutableList()
 | 
			
		||||
                val changes = (
 | 
			
		||||
                    if (step == 0) i until oldList.size else (i until oldList.size step step)
 | 
			
		||||
                ).map { index ->
 | 
			
		||||
                    IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
 | 
			
		||||
                        mutable[index] = it.value
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                val mutableOldList = oldList.toMutableList()
 | 
			
		||||
                mutableOldList.applyDiff(mutable)
 | 
			
		||||
                assertEquals(
 | 
			
		||||
                    mutable,
 | 
			
		||||
                    mutableOldList
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.browser.document
 | 
			
		||||
import org.w3c.dom.*
 | 
			
		||||
 | 
			
		||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
 | 
			
		||||
    lateinit var observer: MutationObserver
 | 
			
		||||
 | 
			
		||||
    observer = MutationObserver { _, _ ->
 | 
			
		||||
        fun checkIfRemoved(node: Node): Boolean {
 | 
			
		||||
            return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (checkIfRemoved(this)) {
 | 
			
		||||
            observer.disconnect()
 | 
			
		||||
            block()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    observer.observe(document, MutationObserverInit(childList = true, subtree = true))
 | 
			
		||||
    return observer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
 | 
			
		||||
    var previousIntersectionRatio = -1f
 | 
			
		||||
    val observer = IntersectionObserver { entries, observer ->
 | 
			
		||||
        entries.forEach {
 | 
			
		||||
            if (previousIntersectionRatio != it.intersectionRatio) {
 | 
			
		||||
                previousIntersectionRatio = it.intersectionRatio.toFloat()
 | 
			
		||||
                it.block(previousIntersectionRatio, observer)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    observer.observe(this)
 | 
			
		||||
    return observer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
 | 
			
		||||
    var previous = -1f
 | 
			
		||||
    onVisibilityChanged { intersectionRatio, observer ->
 | 
			
		||||
        if (previous != intersectionRatio) {
 | 
			
		||||
            if (intersectionRatio > 0 && previous == 0f) {
 | 
			
		||||
                block(observer)
 | 
			
		||||
            }
 | 
			
		||||
            previous = intersectionRatio
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
 | 
			
		||||
    var previous = -1f
 | 
			
		||||
    return onVisibilityChanged { intersectionRatio, observer ->
 | 
			
		||||
        if (previous != intersectionRatio) {
 | 
			
		||||
            if (intersectionRatio == 0f && previous != 0f) {
 | 
			
		||||
                block(observer)
 | 
			
		||||
            }
 | 
			
		||||
            previous = intersectionRatio
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,124 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import org.w3c.dom.DOMRectReadOnly
 | 
			
		||||
import org.w3c.dom.Element
 | 
			
		||||
 | 
			
		||||
external interface IntersectionObserverOptions {
 | 
			
		||||
    /**
 | 
			
		||||
     * An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
 | 
			
		||||
     * considered the viewport. Any part of the target not visible in the visible area of the root is not considered
 | 
			
		||||
     * visible.
 | 
			
		||||
     */
 | 
			
		||||
    var root: Element?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
 | 
			
		||||
     * effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
 | 
			
		||||
     * for the CSS margin property; see The root element and root margin in Intersection Observer API for more
 | 
			
		||||
     * information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
 | 
			
		||||
     */
 | 
			
		||||
    var rootMargin: String?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
 | 
			
		||||
     * total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
 | 
			
		||||
     * the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
 | 
			
		||||
     * Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
 | 
			
		||||
     */
 | 
			
		||||
    var threshold: Array<Number>?
 | 
			
		||||
}
 | 
			
		||||
fun IntersectionObserverOptions(
 | 
			
		||||
    block: IntersectionObserverOptions.() -> Unit = {}
 | 
			
		||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
 | 
			
		||||
 | 
			
		||||
external interface IntersectionObserverEntry {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
 | 
			
		||||
     * the documentation for Element.getBoundingClientRect().
 | 
			
		||||
     */
 | 
			
		||||
    val boundingClientRect: DOMRectReadOnly
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the ratio of the intersectionRect to the boundingClientRect.
 | 
			
		||||
     */
 | 
			
		||||
    val intersectionRatio: Number
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a DOMRectReadOnly representing the target's visible area.
 | 
			
		||||
     */
 | 
			
		||||
    val intersectionRect: DOMRectReadOnly
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
 | 
			
		||||
     * true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
 | 
			
		||||
     * then you know the transition is from intersecting to not-intersecting.
 | 
			
		||||
     */
 | 
			
		||||
    val isIntersecting: Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a DOMRectReadOnly for the intersection observer's root.
 | 
			
		||||
     */
 | 
			
		||||
    val rootBounds: DOMRectReadOnly
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The Element whose intersection with the root changed.
 | 
			
		||||
     */
 | 
			
		||||
    val target: Element
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
 | 
			
		||||
     * IntersectionObserver's time origin.
 | 
			
		||||
     */
 | 
			
		||||
    val time: Double
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
 | 
			
		||||
 * of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
 | 
			
		||||
 */
 | 
			
		||||
external class IntersectionObserver(callback: IntersectionObserverCallback) {
 | 
			
		||||
    constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
 | 
			
		||||
     * was passed to the constructor or its value is null, the top-level document's viewport is used.
 | 
			
		||||
     */
 | 
			
		||||
    val root: Element
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
 | 
			
		||||
     * growing the root for calculation purposes. The value returned by this property may not be the same as the one
 | 
			
		||||
     * specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
 | 
			
		||||
     * expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
 | 
			
		||||
     */
 | 
			
		||||
    val rootMargin: String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
 | 
			
		||||
     * bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
 | 
			
		||||
     * crossed for that target. If no value was passed to the constructor, 0 is used.
 | 
			
		||||
     */
 | 
			
		||||
    val thresholds: Array<Number>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stops the IntersectionObserver object from observing any target.
 | 
			
		||||
     */
 | 
			
		||||
    fun disconnect()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tells the IntersectionObserver a target element to observe.
 | 
			
		||||
     */
 | 
			
		||||
    fun observe(targetElement: Element)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an array of IntersectionObserverEntry objects for all observed targets.
 | 
			
		||||
     */
 | 
			
		||||
    fun takeRecords(): Array<IntersectionObserverEntry>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tells the IntersectionObserver to stop observing a particular target element.
 | 
			
		||||
     */
 | 
			
		||||
    fun unobserve(targetElement: Element)
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import org.khronos.webgl.ArrayBuffer
 | 
			
		||||
import org.w3c.dom.ErrorEvent
 | 
			
		||||
import org.w3c.files.File
 | 
			
		||||
import org.w3c.files.FileReader
 | 
			
		||||
import org.w3c.files.*
 | 
			
		||||
import kotlin.js.Promise
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual typealias MPPFile = File
 | 
			
		||||
 | 
			
		||||
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
 | 
			
		||||
@@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
 | 
			
		||||
    reader.readAsArrayBuffer(this)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun MPPFile.readBytes(): ByteArray {
 | 
			
		||||
    val reader = FileReaderSync()
 | 
			
		||||
    return reader.readAsArrayBuffer(this).toByteArray()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.filename: FileName
 | 
			
		||||
    get() = FileName(name)
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.filesize: Long
 | 
			
		||||
    get() = size.toLong()
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
 | 
			
		||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
    get() = ::readBytes
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
 | 
			
		||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
    get() = ::dirtyReadBytes
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.browser.document
 | 
			
		||||
import org.w3c.dom.*
 | 
			
		||||
import org.w3c.dom.events.Event
 | 
			
		||||
import org.w3c.dom.events.EventListener
 | 
			
		||||
 | 
			
		||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
 | 
			
		||||
    lateinit var observer: MutationObserver
 | 
			
		||||
    val listener = EventListener {
 | 
			
		||||
        val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
 | 
			
		||||
        while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
 | 
			
		||||
            val childrenGettingElement = elementsToCheck.removeFirst()
 | 
			
		||||
            for (i in 0 until childrenGettingElement.childElementCount) {
 | 
			
		||||
                elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (elementsToCheck.isEmpty()) {
 | 
			
		||||
            callback(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (options == null) {
 | 
			
		||||
        document.addEventListener(type, listener)
 | 
			
		||||
    } else {
 | 
			
		||||
        document.addEventListener(type, listener, options)
 | 
			
		||||
    }
 | 
			
		||||
    observer = onRemoved {
 | 
			
		||||
        if (options == null) {
 | 
			
		||||
            document.removeEventListener(type, listener)
 | 
			
		||||
        } else {
 | 
			
		||||
            document.removeEventListener(type, listener, options)
 | 
			
		||||
        }
 | 
			
		||||
        observer.disconnect()
 | 
			
		||||
    }
 | 
			
		||||
    return listener
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.browser.window
 | 
			
		||||
 | 
			
		||||
fun openLink(link: String, target: String = "_blank", features: String = "") {
 | 
			
		||||
    window.open(link, target, features) ?.focus()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.browser.document
 | 
			
		||||
import kotlinx.dom.createElement
 | 
			
		||||
import org.w3c.dom.HTMLElement
 | 
			
		||||
import org.w3c.dom.HTMLInputElement
 | 
			
		||||
import org.w3c.files.get
 | 
			
		||||
 | 
			
		||||
fun selectFile(
 | 
			
		||||
    inputSetup: (HTMLInputElement) -> Unit = {},
 | 
			
		||||
    onFailure: (Throwable) -> Unit = {},
 | 
			
		||||
    onFile: (MPPFile) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    (document.createElement("input") {
 | 
			
		||||
        (this as HTMLInputElement).apply {
 | 
			
		||||
            type = "file"
 | 
			
		||||
            onchange = {
 | 
			
		||||
                runCatching {
 | 
			
		||||
                    files ?.get(0) ?: error("File must not be null")
 | 
			
		||||
                }.onSuccess {
 | 
			
		||||
                    onFile(it)
 | 
			
		||||
                }.onFailure {
 | 
			
		||||
                    onFailure(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            inputSetup(this)
 | 
			
		||||
        }
 | 
			
		||||
    } as HTMLElement).click()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.browser.document
 | 
			
		||||
import org.w3c.dom.HTMLAnchorElement
 | 
			
		||||
 | 
			
		||||
fun triggerDownloadFile(filename: String, fileLink: String) {
 | 
			
		||||
    val hiddenElement = document.createElement("a") as HTMLAnchorElement
 | 
			
		||||
 | 
			
		||||
    hiddenElement.href = fileLink
 | 
			
		||||
    hiddenElement.target = "_blank"
 | 
			
		||||
    hiddenElement.download = filename
 | 
			
		||||
    hiddenElement.click()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual typealias MPPFile = File
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.filename: FileName
 | 
			
		||||
    get() = FileName(name)
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.filesize: Long
 | 
			
		||||
    get() = length()
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
    get() = ::readBytes
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
    get() = {
 | 
			
		||||
        doInIO {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,17 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
 | 
			
		||||
                api libs.kt.coroutines
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jsMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        androidMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
 | 
			
		||||
                api libs.kt.coroutines.android
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    id "org.jetbrains.kotlin.multiplatform"
 | 
			
		||||
    id "org.jetbrains.kotlin.plugin.serialization"
 | 
			
		||||
    id "com.android.library"
 | 
			
		||||
    alias(libs.plugins.jb.compose)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api libs.kt.coroutines
 | 
			
		||||
                api project(":micro_utils.coroutines")
 | 
			
		||||
                api project(":micro_utils.common.compose")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.MutableState
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
fun <T> Flow<T>.toMutableState(
 | 
			
		||||
    initial: T,
 | 
			
		||||
    scope: CoroutineScope
 | 
			
		||||
): MutableState<T> {
 | 
			
		||||
    val state = mutableStateOf(initial)
 | 
			
		||||
    subscribeSafelyWithoutExceptions(scope) { state.value = it }
 | 
			
		||||
    return state
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <T> StateFlow<T>.toMutableState(
 | 
			
		||||
    scope: CoroutineScope
 | 
			
		||||
): MutableState<T> = toMutableState(value, scope)
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.job
 | 
			
		||||
import kotlin.coroutines.CoroutineContext
 | 
			
		||||
 | 
			
		||||
fun Composition.linkWithJob(job: Job) {
 | 
			
		||||
    job.invokeOnCompletion {
 | 
			
		||||
        this@linkWithJob.dispose()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import dev.inmo.micro_utils.common.compose.linkWithElement
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import org.jetbrains.compose.web.dom.DOMScope
 | 
			
		||||
import org.w3c.dom.Element
 | 
			
		||||
 | 
			
		||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
 | 
			
		||||
    root: TElement,
 | 
			
		||||
    monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
 | 
			
		||||
    content: @Composable DOMScope<TElement>.() -> Unit
 | 
			
		||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
 | 
			
		||||
    linkWithContext(
 | 
			
		||||
        currentCoroutineContext()
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
 | 
			
		||||
    root: TElement,
 | 
			
		||||
    monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
 | 
			
		||||
    content: @Composable DOMScope<TElement>.() -> Unit
 | 
			
		||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
 | 
			
		||||
    linkWithContext(currentCoroutineContext())
 | 
			
		||||
    linkWithElement(root)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
 | 
			
		||||
@@ -0,0 +1,94 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.channels.BufferOverflow
 | 
			
		||||
import kotlinx.coroutines.channels.Channel
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
private sealed interface AccumulatorFlowStep
 | 
			
		||||
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
 | 
			
		||||
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
 | 
			
		||||
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
 | 
			
		||||
 *
 | 
			
		||||
 * * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new
 | 
			
		||||
 * [FlowCollector]s until anybody will handle it
 | 
			
		||||
 * * Here there are an [activeData] where data [T] will be stored until somebody will handle it
 | 
			
		||||
 */
 | 
			
		||||
class AccumulatorFlow<T>(
 | 
			
		||||
    sourceDataFlow: Flow<T>,
 | 
			
		||||
    scope: CoroutineScope
 | 
			
		||||
) : AbstractFlow<T>() {
 | 
			
		||||
    private val subscope = scope.LinkedSupervisorScope()
 | 
			
		||||
    private val activeData = ArrayDeque<T>()
 | 
			
		||||
    private val dataMutex = Mutex()
 | 
			
		||||
    private val channelsForBroadcast = mutableListOf<Channel<Any>>()
 | 
			
		||||
    private val channelsMutex = Mutex()
 | 
			
		||||
    private val steps = subscope.actor<AccumulatorFlowStep> { step ->
 | 
			
		||||
        when (step) {
 | 
			
		||||
            is DataRetrievedAccumulatorFlowStep -> {
 | 
			
		||||
                if (activeData.first() === step.data) {
 | 
			
		||||
                    dataMutex.withLock {
 | 
			
		||||
                        activeData.removeFirst()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is SubscribeAccumulatorFlowStep -> channelsMutex.withLock {
 | 
			
		||||
                channelsForBroadcast.add(step.channel)
 | 
			
		||||
                dataMutex.withLock {
 | 
			
		||||
                    val dataToSend = activeData.toList()
 | 
			
		||||
                    safelyWithoutExceptions {
 | 
			
		||||
                        dataToSend.forEach { step.channel.send(it as Any) }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock {
 | 
			
		||||
                channelsForBroadcast.remove(step.channel)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) {
 | 
			
		||||
        dataMutex.withLock {
 | 
			
		||||
            activeData.addLast(it)
 | 
			
		||||
        }
 | 
			
		||||
        channelsMutex.withLock {
 | 
			
		||||
            channelsForBroadcast.forEach { channel ->
 | 
			
		||||
                safelyWithResult {
 | 
			
		||||
                    channel.send(it as Any)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun collectSafely(collector: FlowCollector<T>) {
 | 
			
		||||
        val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
 | 
			
		||||
        steps.send(SubscribeAccumulatorFlowStep(channel))
 | 
			
		||||
        for (data in channel) {
 | 
			
		||||
            try {
 | 
			
		||||
                collector.emit(data as T)
 | 
			
		||||
                steps.send(DataRetrievedAccumulatorFlowStep(data))
 | 
			
		||||
            } finally {
 | 
			
		||||
                channel.cancel()
 | 
			
		||||
                steps.send(UnsubscribeAccumulatorFlowStep(channel))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates [AccumulatorFlow] using [this] as base [Flow]
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
 | 
			
		||||
    return AccumulatorFlow(this, scope)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
 | 
			
		||||
    return receiveAsFlow().accumulatorFlow(scope)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.onRemoved
 | 
			
		||||
import dev.inmo.micro_utils.common.onVisibilityChanged
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import org.w3c.dom.Element
 | 
			
		||||
 | 
			
		||||
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
 | 
			
		||||
    var previousData: Boolean? = null
 | 
			
		||||
 | 
			
		||||
    val observer = onVisibilityChanged { intersectionRatio, _ ->
 | 
			
		||||
        val currentData = intersectionRatio > 0
 | 
			
		||||
        if (currentData != previousData) {
 | 
			
		||||
            trySend(currentData)
 | 
			
		||||
        }
 | 
			
		||||
        previousData = currentData
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val removeObserver = onRemoved {
 | 
			
		||||
        observer.disconnect()
 | 
			
		||||
        close()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invokeOnClose {
 | 
			
		||||
        observer.disconnect()
 | 
			
		||||
        removeObserver.disconnect()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.common.selectFile
 | 
			
		||||
import kotlinx.coroutines.CompletableDeferred
 | 
			
		||||
import org.w3c.dom.HTMLInputElement
 | 
			
		||||
 | 
			
		||||
suspend fun selectFileOrThrow(
 | 
			
		||||
    inputSetup: (HTMLInputElement) -> Unit = {}
 | 
			
		||||
): MPPFile {
 | 
			
		||||
    val result = CompletableDeferred<MPPFile>()
 | 
			
		||||
 | 
			
		||||
    selectFile(
 | 
			
		||||
        inputSetup,
 | 
			
		||||
        {
 | 
			
		||||
            result.completeExceptionally(it)
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        result.complete(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result.await()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun selectFileOrNull(
 | 
			
		||||
    inputSetup: (HTMLInputElement) -> Unit = {},
 | 
			
		||||
    onFailure: (Throwable) -> Unit = {}
 | 
			
		||||
): MPPFile? {
 | 
			
		||||
    val result = CompletableDeferred<MPPFile?>()
 | 
			
		||||
 | 
			
		||||
    selectFile(
 | 
			
		||||
        inputSetup,
 | 
			
		||||
        {
 | 
			
		||||
            result.complete(null)
 | 
			
		||||
            onFailure(it)
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        result.complete(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result.await()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.crypto
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto
 | 
			
		||||
import java.math.BigInteger
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
 | 
			
		||||
    1,
 | 
			
		||||
    MessageDigest.getInstance("MD5").digest(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,12 @@ ext {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion "$android_compileSdkVersion".toInteger()
 | 
			
		||||
    buildToolsVersion "$android_buildToolsVersion"
 | 
			
		||||
    compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
 | 
			
		||||
    buildToolsVersion libs.versions.android.props.buildTools.get()
 | 
			
		||||
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        minSdkVersion "$android_minSdkVersion".toInteger()
 | 
			
		||||
        targetSdkVersion "$android_compileSdkVersion".toInteger()
 | 
			
		||||
        minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
 | 
			
		||||
        targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
 | 
			
		||||
        versionCode "${android_code_version}".toInteger()
 | 
			
		||||
        versionName "$version"
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@ repositories {
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    jvm()
 | 
			
		||||
    js(IR) {
 | 
			
		||||
        browser()
 | 
			
		||||
        nodejs()
 | 
			
		||||
    }
 | 
			
		||||
//    js(IR) {
 | 
			
		||||
//        browser()
 | 
			
		||||
//        nodejs()
 | 
			
		||||
//    }
 | 
			
		||||
    android {}
 | 
			
		||||
 | 
			
		||||
    sourceSets {
 | 
			
		||||
@@ -29,7 +29,7 @@ kotlin {
 | 
			
		||||
                        it != project
 | 
			
		||||
                        && it.hasProperty("kotlin")
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
 | 
			
		||||
//                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("androidMain") }
 | 
			
		||||
                    ) {
 | 
			
		||||
@@ -38,22 +38,22 @@ kotlin {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jsMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('stdlib')
 | 
			
		||||
//        jsMain {
 | 
			
		||||
//            dependencies {
 | 
			
		||||
//                implementation kotlin('stdlib')
 | 
			
		||||
 | 
			
		||||
                project.parent.subprojects.forEach {
 | 
			
		||||
                    if (
 | 
			
		||||
                        it != project
 | 
			
		||||
                        && it.hasProperty("kotlin")
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
 | 
			
		||||
                    ) {
 | 
			
		||||
                        api it
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
//                project.parent.subprojects.forEach {
 | 
			
		||||
//                    if (
 | 
			
		||||
//                        it != project
 | 
			
		||||
//                        && it.hasProperty("kotlin")
 | 
			
		||||
//                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
 | 
			
		||||
//                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
 | 
			
		||||
//                    ) {
 | 
			
		||||
//                        api it
 | 
			
		||||
//                    }
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
        jvmMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('stdlib')
 | 
			
		||||
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
 | 
			
		||||
            sourceRoots.setFrom(findSourcesWithName("commonMain"))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        named("jsMain") {
 | 
			
		||||
            sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
 | 
			
		||||
        }
 | 
			
		||||
//        named("jsMain") {
 | 
			
		||||
//            sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
 | 
			
		||||
//        }
 | 
			
		||||
 | 
			
		||||
        named("jvmMain") {
 | 
			
		||||
            sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ allprojects {
 | 
			
		||||
        releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
 | 
			
		||||
 | 
			
		||||
        mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
 | 
			
		||||
        mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
 | 
			
		||||
        mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
 | 
			
		||||
        mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
                api project(":micro_utils.coroutines")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import kotlin.reflect.KClass
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Define checkable holder which can be used to precheck that this handler may handle incoming [State]
 | 
			
		||||
 */
 | 
			
		||||
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
 | 
			
		||||
    suspend fun checkHandleable(state: O): Boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
 | 
			
		||||
 * casting in [handleState]
 | 
			
		||||
 */
 | 
			
		||||
class CustomizableHandlerHolder<I : O, O : State>(
 | 
			
		||||
    private val delegateTo: StatesHandler<I, O>,
 | 
			
		||||
    private val filter: suspend (state: O) -> Boolean
 | 
			
		||||
) : CheckableHandlerHolder<I, O> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
 | 
			
		||||
     * [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun checkHandleable(state: O) = filter(state)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
 | 
			
		||||
     * to be sure that this [StatesHandlerHolder] will be able to handle [state]
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun StatesMachine<in O>.handleState(state: I): O? {
 | 
			
		||||
        return delegateTo.run { handleState(state) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun <I : O, O : State> CheckableHandlerHolder(
 | 
			
		||||
    inputKlass: KClass<I>,
 | 
			
		||||
    strict: Boolean = false,
 | 
			
		||||
    delegateTo: StatesHandler<I, O>
 | 
			
		||||
) = CustomizableHandlerHolder(
 | 
			
		||||
    StatesHandler<O, O> {
 | 
			
		||||
        delegateTo.run { handleState(it as I) }
 | 
			
		||||
    },
 | 
			
		||||
    if (strict) {
 | 
			
		||||
        { it::class == inputKlass }
 | 
			
		||||
    } else {
 | 
			
		||||
        { inputKlass.isInstance(it) }
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
 | 
			
		||||
fun <I : O, O : State> StateHandlerHolder(
 | 
			
		||||
    inputKlass: KClass<I>,
 | 
			
		||||
    strict: Boolean = false,
 | 
			
		||||
    delegateTo: StatesHandler<I, O>
 | 
			
		||||
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
 | 
			
		||||
 | 
			
		||||
inline fun <reified I : O, O : State> CheckableHandlerHolder(
 | 
			
		||||
    strict: Boolean = false,
 | 
			
		||||
    delegateTo: StatesHandler<I, O>
 | 
			
		||||
) = CheckableHandlerHolder(I::class, strict, delegateTo)
 | 
			
		||||
 | 
			
		||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
 | 
			
		||||
inline fun <reified I : O, O : State> StateHandlerHolder(
 | 
			
		||||
    strict: Boolean = false,
 | 
			
		||||
    delegateTo: StatesHandler<I, O>
 | 
			
		||||
) = CheckableHandlerHolder(strict, delegateTo)
 | 
			
		||||
 | 
			
		||||
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
 | 
			
		||||
    strict: Boolean = true
 | 
			
		||||
) = CheckableHandlerHolder<I, O>(
 | 
			
		||||
    I::class,
 | 
			
		||||
    strict,
 | 
			
		||||
    this
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
 | 
			
		||||
    noinline filter: suspend (state: State) -> Boolean
 | 
			
		||||
) = CustomizableHandlerHolder<O, O>(
 | 
			
		||||
    { this@holder.run { handleState(it as I) } },
 | 
			
		||||
    filter
 | 
			
		||||
)
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import kotlin.reflect.KClass
 | 
			
		||||
 | 
			
		||||
class StateHandlerHolder<I : State>(
 | 
			
		||||
    private val inputKlass: KClass<I>,
 | 
			
		||||
    private val strict: Boolean = false,
 | 
			
		||||
    private val delegateTo: StatesHandler<I>
 | 
			
		||||
) : StatesHandler<State> {
 | 
			
		||||
    fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
 | 
			
		||||
 | 
			
		||||
    override suspend fun StatesMachine.handleState(state: State): State? {
 | 
			
		||||
        return delegateTo.run { handleState(state as I) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
fun interface StatesHandler<I : State> {
 | 
			
		||||
    suspend fun StatesMachine.handleState(state: I): State?
 | 
			
		||||
/**
 | 
			
		||||
 * Default realization of states handler
 | 
			
		||||
 */
 | 
			
		||||
fun interface StatesHandler<I : State, O: State> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
 | 
			
		||||
     * [State] it is assumed that chain is not completed.
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun StatesMachine<in O>.handleState(state: I): O?
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,70 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.Optional
 | 
			
		||||
import dev.inmo.micro_utils.common.onPresented
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.flow.asFlow
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
private suspend fun <I : State> StatesMachine.launchStateHandling(
 | 
			
		||||
    state: State,
 | 
			
		||||
    handlers: List<StateHandlerHolder<out I>>
 | 
			
		||||
): State? {
 | 
			
		||||
/**
 | 
			
		||||
 * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
 | 
			
		||||
 * [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
 | 
			
		||||
 * handling until [start] method will be called
 | 
			
		||||
 */
 | 
			
		||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
 | 
			
		||||
    suspend fun launchStateHandling(
 | 
			
		||||
        state: T,
 | 
			
		||||
        handlers: List<CheckableHandlerHolder<in T, T>>
 | 
			
		||||
    ): T? {
 | 
			
		||||
        return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
 | 
			
		||||
            handleState(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Starts handling of [State]s
 | 
			
		||||
     */
 | 
			
		||||
    fun start(scope: CoroutineScope): Job
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start chain of [State]s witn [state]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun startChain(state: T)
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Creates [DefaultStatesMachine]
 | 
			
		||||
         */
 | 
			
		||||
        operator fun <T: State> invoke(
 | 
			
		||||
            statesManager: StatesManager<T>,
 | 
			
		||||
            handlers: List<CheckableHandlerHolder<in T, T>>
 | 
			
		||||
        ) = DefaultStatesMachine(statesManager, handlers)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class StatesMachine (
 | 
			
		||||
    private val statesManager: StatesManager,
 | 
			
		||||
    private val handlers: List<StateHandlerHolder<*>>
 | 
			
		||||
) : StatesHandler<State> {
 | 
			
		||||
    override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
 | 
			
		||||
/**
 | 
			
		||||
 * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
 | 
			
		||||
 * resolving, and uses [launchStateHandling] for [State] handling.
 | 
			
		||||
 *
 | 
			
		||||
 * This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
 | 
			
		||||
 */
 | 
			
		||||
open class DefaultStatesMachine <T: State>(
 | 
			
		||||
    protected val statesManager: StatesManager<T>,
 | 
			
		||||
    protected val handlers: List<CheckableHandlerHolder<in T, T>>,
 | 
			
		||||
) : StatesMachine<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Will call [launchStateHandling] for state handling
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
 | 
			
		||||
 | 
			
		||||
    fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
 | 
			
		||||
        val statePerformer: suspend (State) -> Unit = { state: State ->
 | 
			
		||||
    /**
 | 
			
		||||
     * This
 | 
			
		||||
     */
 | 
			
		||||
    protected val statesJobs = mutableMapOf<T, Job>()
 | 
			
		||||
    protected val statesJobsMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    protected open suspend fun performUpdate(state: T) {
 | 
			
		||||
        val newState = launchStateHandling(state, handlers)
 | 
			
		||||
        if (newState != null) {
 | 
			
		||||
            statesManager.update(state, newState)
 | 
			
		||||
@@ -28,19 +72,59 @@ class StatesMachine (
 | 
			
		||||
            statesManager.endChain(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
 | 
			
		||||
        statesJobsMutex.withLock {
 | 
			
		||||
            statesJobs[actualState] ?.cancel()
 | 
			
		||||
            statesJobs[actualState] = scope.launch {
 | 
			
		||||
                performUpdate(actualState)
 | 
			
		||||
            }.also { job ->
 | 
			
		||||
                job.invokeOnCompletion { _ ->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        statesJobsMutex.withLock {
 | 
			
		||||
                            if (statesJobs[actualState] == job) {
 | 
			
		||||
                                statesJobs.remove(actualState)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Launch handling of states. On [statesManager] [StatesManager.onStartChain],
 | 
			
		||||
     * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
 | 
			
		||||
     * [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
 | 
			
		||||
     * [StatesManager.endChain].
 | 
			
		||||
     */
 | 
			
		||||
    override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
 | 
			
		||||
        statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
 | 
			
		||||
            launch { statePerformer(it) }
 | 
			
		||||
            launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
        statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
 | 
			
		||||
            launch { statePerformer(it.second) }
 | 
			
		||||
            launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
        statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
 | 
			
		||||
            launch {
 | 
			
		||||
                statesJobsMutex.withLock {
 | 
			
		||||
                    val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
 | 
			
		||||
                    if (stateInMap === removedState) {
 | 
			
		||||
                        statesJobs[stateInMap] ?.cancel()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        statesManager.getActiveStates().forEach {
 | 
			
		||||
            launch { statePerformer(it) }
 | 
			
		||||
            launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun startChain(state: State) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Just calls [StatesManager.startChain] of [statesManager]
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun startChain(state: T) {
 | 
			
		||||
        statesManager.startChain(state)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,92 +1,30 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
interface StatesManager {
 | 
			
		||||
    val onChainStateUpdated: Flow<Pair<State, State>>
 | 
			
		||||
    val onStartChain: Flow<State>
 | 
			
		||||
    val onEndChain: Flow<State>
 | 
			
		||||
interface StatesManager<T : State> {
 | 
			
		||||
    val onChainStateUpdated: Flow<Pair<T, T>>
 | 
			
		||||
    val onStartChain: Flow<T>
 | 
			
		||||
    val onEndChain: Flow<T>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Must set current set using [State.context]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun update(old: State, new: State)
 | 
			
		||||
    suspend fun update(old: T, new: T)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
 | 
			
		||||
     * busy by the other [State]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun startChain(state: State)
 | 
			
		||||
    suspend fun startChain(state: T)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
 | 
			
		||||
     * ignored
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun endChain(state: State)
 | 
			
		||||
    suspend fun endChain(state: T)
 | 
			
		||||
 | 
			
		||||
    suspend fun getActiveStates(): List<State>
 | 
			
		||||
    suspend fun getActiveStates(): List<T>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
 | 
			
		||||
 * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
 | 
			
		||||
 * new state by using [endChain] with that state
 | 
			
		||||
 */
 | 
			
		||||
class InMemoryStatesManager(
 | 
			
		||||
    private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
 | 
			
		||||
) : StatesManager {
 | 
			
		||||
    private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
 | 
			
		||||
    override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
 | 
			
		||||
    private val _onStartChain = MutableSharedFlow<State>(0)
 | 
			
		||||
    override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
 | 
			
		||||
    private val _onEndChain = MutableSharedFlow<State>(0)
 | 
			
		||||
    override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    private val contextsToStates = mutableMapOf<Any, State>()
 | 
			
		||||
    private val mapMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(old: State, new: State) = mapMutex.withLock {
 | 
			
		||||
        when {
 | 
			
		||||
            contextsToStates[old.context] != old -> return@withLock
 | 
			
		||||
            old.context == new.context || !contextsToStates.containsKey(new.context) -> {
 | 
			
		||||
                contextsToStates[old.context] = new
 | 
			
		||||
                _onChainStateUpdated.emit(old to new)
 | 
			
		||||
            }
 | 
			
		||||
            else -> {
 | 
			
		||||
                val stateOnNewOneContext = contextsToStates.getValue(new.context)
 | 
			
		||||
                if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
 | 
			
		||||
                    endChainWithoutLock(stateOnNewOneContext)
 | 
			
		||||
                    contextsToStates.remove(old.context)
 | 
			
		||||
                    contextsToStates[new.context] = new
 | 
			
		||||
                    _onChainStateUpdated.emit(old to new)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun startChain(state: State) = mapMutex.withLock {
 | 
			
		||||
        if (!contextsToStates.containsKey(state.context)) {
 | 
			
		||||
            contextsToStates[state.context] = state
 | 
			
		||||
            _onStartChain.emit(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun endChainWithoutLock(state: State) {
 | 
			
		||||
        if (contextsToStates[state.context] == state) {
 | 
			
		||||
            contextsToStates.remove(state.context)
 | 
			
		||||
            _onEndChain.emit(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun endChain(state: State) {
 | 
			
		||||
        mapMutex.withLock {
 | 
			
		||||
            endChainWithoutLock(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.*
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
 | 
			
		||||
 * must be able to perform update of chain in internal [StatesManager]
 | 
			
		||||
 */
 | 
			
		||||
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
 | 
			
		||||
     * in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
 | 
			
		||||
     * [StatesManager.update] function
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun updateChain(currentState: T, newState: T)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
open class DefaultUpdatableStatesMachine<T : State>(
 | 
			
		||||
    statesManager: StatesManager<T>,
 | 
			
		||||
    handlers: List<CheckableHandlerHolder<in T, T>>,
 | 
			
		||||
) : DefaultStatesMachine<T>(
 | 
			
		||||
    statesManager,
 | 
			
		||||
    handlers
 | 
			
		||||
), UpdatableStatesMachine<T> {
 | 
			
		||||
    protected val jobsStates = mutableMapOf<Job, T>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
 | 
			
		||||
     * [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
 | 
			
		||||
     * other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
 | 
			
		||||
     * states is solved in [shouldReplaceJob] and can be rewritten in subclasses
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
 | 
			
		||||
        statesJobsMutex.withLock {
 | 
			
		||||
            if (compare(previousState, actualState)) {
 | 
			
		||||
                statesJobs[actualState] ?.cancel()
 | 
			
		||||
            }
 | 
			
		||||
            val job = previousState.mapOnPresented {
 | 
			
		||||
                statesJobs.remove(it)
 | 
			
		||||
            } ?.takeIf { it.isActive } ?: scope.launch {
 | 
			
		||||
                performUpdate(actualState)
 | 
			
		||||
            }.also { job ->
 | 
			
		||||
                job.invokeOnCompletion { _ ->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        statesJobsMutex.withLock {
 | 
			
		||||
                            statesJobs.remove(
 | 
			
		||||
                                jobsStates[job] ?: return@withLock
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            jobsStates.remove(job)
 | 
			
		||||
            statesJobs[actualState] = job
 | 
			
		||||
            jobsStates[job] = actualState
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compare if [previous] potentially lead to the same behaviour with [new]
 | 
			
		||||
     */
 | 
			
		||||
    protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
 | 
			
		||||
 | 
			
		||||
    @Deprecated("Overwrite shouldReplaceJob instead")
 | 
			
		||||
    protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
 | 
			
		||||
 | 
			
		||||
    override suspend fun updateChain(currentState: T, newState: T) {
 | 
			
		||||
        statesManager.update(currentState, newState)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +1,61 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common.dsl
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.*
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo
 | 
			
		||||
import kotlin.reflect.KClass
 | 
			
		||||
 | 
			
		||||
class FSMBuilder(
 | 
			
		||||
    var statesManager: StatesManager = InMemoryStatesManager()
 | 
			
		||||
) {
 | 
			
		||||
    private var states = mutableListOf<StateHandlerHolder<*>>()
 | 
			
		||||
 | 
			
		||||
    fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
 | 
			
		||||
        states.add(StateHandlerHolder(kClass, false, handler))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
 | 
			
		||||
        states.add(StateHandlerHolder(kClass, true, handler))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun build() = StatesMachine(
 | 
			
		||||
class FSMBuilder<T : State>(
 | 
			
		||||
    var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
 | 
			
		||||
    val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
 | 
			
		||||
        StatesMachine(
 | 
			
		||||
            statesManager,
 | 
			
		||||
        states.toList()
 | 
			
		||||
            states
 | 
			
		||||
        )
 | 
			
		||||
    },
 | 
			
		||||
    var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
 | 
			
		||||
) {
 | 
			
		||||
    private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
 | 
			
		||||
 | 
			
		||||
    fun add(handler: CheckableHandlerHolder<T, T>) {
 | 
			
		||||
        states.add(handler)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
 | 
			
		||||
        add(CheckableHandlerHolder(kClass, false, handler))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
 | 
			
		||||
        add(handler.holder(filter))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
 | 
			
		||||
        states.add(CheckableHandlerHolder(kClass, true, handler))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
 | 
			
		||||
        add(I::class, handler)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
 | 
			
		||||
        addStrict(I::class, handler)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inline fun <reified I : T> doWhen(
 | 
			
		||||
        noinline filter: suspend (state: State) -> Boolean,
 | 
			
		||||
        handler: StatesHandler<I, T>
 | 
			
		||||
    ) {
 | 
			
		||||
        add(filter, handler)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun build() = fsmBuilder(
 | 
			
		||||
        statesManager,
 | 
			
		||||
        states.toList().let { list ->
 | 
			
		||||
            defaultStateHandler ?.let { list + it.holder { true } } ?: list
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
 | 
			
		||||
    add(I::class, handler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
 | 
			
		||||
    addStrict(I::class, handler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun buildFSM(
 | 
			
		||||
    block: FSMBuilder.() -> Unit
 | 
			
		||||
): StatesMachine = FSMBuilder().apply(block).build()
 | 
			
		||||
fun <T : State> buildFSM(
 | 
			
		||||
    block: FSMBuilder<T>.() -> Unit
 | 
			
		||||
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common.managers
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.State
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.StatesManager
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implement this repo if you want to use some custom repo for [DefaultStatesManager]
 | 
			
		||||
 */
 | 
			
		||||
interface DefaultStatesManagerRepo<T : State> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Must save [state] as current state of chain with [State.context] of [state]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun set(state: T)
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
 | 
			
		||||
     * NOT be removed
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun removeState(state: T)
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Current list of available and saved states
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun getStates(): List<T>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Current state by [context]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun getContextState(context: Any): T?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Current state by [context]
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun contains(context: Any): Boolean = getContextState(context) != null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
 | 
			
		||||
 * any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
 | 
			
		||||
 * [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
 | 
			
		||||
 * @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
 | 
			
		||||
 * this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
 | 
			
		||||
 * with [endChain] with current state
 | 
			
		||||
 * @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
 | 
			
		||||
 * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
 | 
			
		||||
 * new state by using [endChain] with that state
 | 
			
		||||
 */
 | 
			
		||||
open class DefaultStatesManager<T : State>(
 | 
			
		||||
    protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
 | 
			
		||||
    protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
 | 
			
		||||
    protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
 | 
			
		||||
) : StatesManager<T> {
 | 
			
		||||
    protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
 | 
			
		||||
    override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
 | 
			
		||||
    protected val _onStartChain = MutableSharedFlow<T>(0)
 | 
			
		||||
    override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
 | 
			
		||||
    protected val _onEndChain = MutableSharedFlow<T>(0)
 | 
			
		||||
    override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    protected val mapMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        repo: DefaultStatesManagerRepo<T>,
 | 
			
		||||
        onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
 | 
			
		||||
    ) : this (
 | 
			
		||||
        repo,
 | 
			
		||||
        onUpdateContextsConflictResolver = onContextsConflictResolver
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(old: T, new: T) = mapMutex.withLock {
 | 
			
		||||
        val stateByOldContext: T? = repo.getContextState(old.context)
 | 
			
		||||
        when {
 | 
			
		||||
            stateByOldContext != old -> return@withLock
 | 
			
		||||
            stateByOldContext == null || old.context == new.context -> {
 | 
			
		||||
                repo.removeState(old)
 | 
			
		||||
                repo.set(new)
 | 
			
		||||
                _onChainStateUpdated.emit(old to new)
 | 
			
		||||
            }
 | 
			
		||||
            else -> {
 | 
			
		||||
                val stateOnNewOneContext = repo.getContextState(new.context)
 | 
			
		||||
                if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
 | 
			
		||||
                    stateOnNewOneContext ?.let { endChainWithoutLock(it) }
 | 
			
		||||
                    repo.removeState(old)
 | 
			
		||||
                    repo.set(new)
 | 
			
		||||
                    _onChainStateUpdated.emit(old to new)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun startChain(state: T) = mapMutex.withLock {
 | 
			
		||||
        val stateOnContext = repo.getContextState(state.context)
 | 
			
		||||
        if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
 | 
			
		||||
            stateOnContext ?.let {
 | 
			
		||||
                endChainWithoutLock(it)
 | 
			
		||||
            }
 | 
			
		||||
            repo.set(state)
 | 
			
		||||
            _onStartChain.emit(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected open suspend fun endChainWithoutLock(state: T) {
 | 
			
		||||
        if (repo.getContextState(state.context) == state) {
 | 
			
		||||
            repo.removeState(state)
 | 
			
		||||
            _onEndChain.emit(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun endChain(state: T) {
 | 
			
		||||
        mapMutex.withLock {
 | 
			
		||||
            endChainWithoutLock(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getActiveStates(): List<T> = repo.getStates()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common.managers
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.State
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
 | 
			
		||||
 * functionality
 | 
			
		||||
 */
 | 
			
		||||
class InMemoryDefaultStatesManagerRepo<T : State>(
 | 
			
		||||
    private val map: MutableMap<Any, T> = mutableMapOf()
 | 
			
		||||
) : DefaultStatesManagerRepo<T> {
 | 
			
		||||
    override suspend fun set(state: T) {
 | 
			
		||||
        map[state.context] = state
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun removeState(state: T) {
 | 
			
		||||
        map.remove(state.context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getStates(): List<T> = map.values.toList()
 | 
			
		||||
 | 
			
		||||
    override suspend fun getContextState(context: Any): T? = map[context]
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(context: Any): Boolean = map.contains(context)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common.managers
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.State
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
 | 
			
		||||
 * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
 | 
			
		||||
 * new state by using [endChain] with that state
 | 
			
		||||
 */
 | 
			
		||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
 | 
			
		||||
fun <T: State> InMemoryStatesManager(
 | 
			
		||||
    onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
 | 
			
		||||
    onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
 | 
			
		||||
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.*
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
 | 
			
		||||
sealed interface TrafficLightState : State {
 | 
			
		||||
@@ -25,9 +26,9 @@ class PlayableMain {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val statesManager = InMemoryStatesManager()
 | 
			
		||||
            val statesManager = DefaultStatesManager<TrafficLightState>()
 | 
			
		||||
 | 
			
		||||
            val machine = buildFSM {
 | 
			
		||||
            val machine = buildFSM<TrafficLightState> {
 | 
			
		||||
                strictlyOn<GreenCommon> {
 | 
			
		||||
                    delay(1000L)
 | 
			
		||||
                    YellowCommon(it.context).also(::println)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.repos.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.State
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import dev.inmo.micro_utils.repos.pagination.getAll
 | 
			
		||||
 | 
			
		||||
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
 | 
			
		||||
    private val keyValueRepo: KeyValueRepo<Any, T>
 | 
			
		||||
) : DefaultStatesManagerRepo<T> {
 | 
			
		||||
    override suspend fun set(state: T) {
 | 
			
		||||
        keyValueRepo.set(state.context, state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun removeState(state: T) {
 | 
			
		||||
        if (keyValueRepo.get(state.context) == state) {
 | 
			
		||||
            keyValueRepo.unset(state.context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second }
 | 
			
		||||
    override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.repos.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.State
 | 
			
		||||
import dev.inmo.micro_utils.fsm.common.StatesManager
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import dev.inmo.micro_utils.repos.mappers.withMapper
 | 
			
		||||
import dev.inmo.micro_utils.repos.pagination.getAll
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
class KeyValueBasedStatesManager(
 | 
			
		||||
    private val keyValueRepo: KeyValueRepo<Any, State>,
 | 
			
		||||
    private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
 | 
			
		||||
) : StatesManager {
 | 
			
		||||
    private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
 | 
			
		||||
    override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
 | 
			
		||||
    private val _onEndChain = MutableSharedFlow<State>(0)
 | 
			
		||||
    override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
 | 
			
		||||
 | 
			
		||||
    private val mutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(old: State, new: State) {
 | 
			
		||||
        mutex.withLock {
 | 
			
		||||
            when {
 | 
			
		||||
                keyValueRepo.get(old.context) != old -> return@withLock
 | 
			
		||||
                old.context == new.context || !keyValueRepo.contains(new.context) -> {
 | 
			
		||||
                    keyValueRepo.set(old.context, new)
 | 
			
		||||
                    _onChainStateUpdated.emit(old to new)
 | 
			
		||||
                }
 | 
			
		||||
                else -> {
 | 
			
		||||
                    val stateOnNewOneContext = keyValueRepo.get(new.context)!!
 | 
			
		||||
                    if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
 | 
			
		||||
                        endChainWithoutLock(stateOnNewOneContext)
 | 
			
		||||
                        keyValueRepo.unset(old.context)
 | 
			
		||||
                        keyValueRepo.set(new.context, new)
 | 
			
		||||
                        _onChainStateUpdated.emit(old to new)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun startChain(state: State) {
 | 
			
		||||
        if (!keyValueRepo.contains(state.context)) {
 | 
			
		||||
            keyValueRepo.set(state.context, state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun endChainWithoutLock(state: State) {
 | 
			
		||||
        if (keyValueRepo.get(state.context) == state) {
 | 
			
		||||
            keyValueRepo.unset(state.context)
 | 
			
		||||
            _onEndChain.emit(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun endChain(state: State) {
 | 
			
		||||
        mutex.withLock { endChainWithoutLock(state) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getActiveStates(): List<State> {
 | 
			
		||||
        return keyValueRepo.getAll { keys(it) }.map { it.second }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
 | 
			
		||||
    targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
 | 
			
		||||
    noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
 | 
			
		||||
    noinline stateToOutTransformer: suspend State.() -> TargetStateType,
 | 
			
		||||
    noinline outToContextTransformer: suspend TargetContextType.() -> Any,
 | 
			
		||||
    noinline outToStateTransformer: suspend TargetStateType.() -> State,
 | 
			
		||||
) = KeyValueBasedStatesManager(
 | 
			
		||||
    targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
 | 
			
		||||
        contextToOutTransformer,
 | 
			
		||||
        stateToOutTransformer,
 | 
			
		||||
        outToContextTransformer,
 | 
			
		||||
        outToStateTransformer
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
@@ -7,43 +7,12 @@ android.useAndroidX=true
 | 
			
		||||
android.enableJetifier=true
 | 
			
		||||
org.gradle.jvmargs=-Xmx2g
 | 
			
		||||
 | 
			
		||||
kotlin_version=1.5.31
 | 
			
		||||
kotlin_coroutines_version=1.5.2
 | 
			
		||||
kotlin_serialisation_core_version=1.3.0
 | 
			
		||||
kotlin_exposed_version=0.35.1
 | 
			
		||||
 | 
			
		||||
ktor_version=1.6.3
 | 
			
		||||
 | 
			
		||||
klockVersion=2.4.3
 | 
			
		||||
 | 
			
		||||
github_release_plugin_version=2.2.12
 | 
			
		||||
 | 
			
		||||
uuidVersion=0.3.1
 | 
			
		||||
 | 
			
		||||
# ANDROID
 | 
			
		||||
 | 
			
		||||
core_ktx_version=1.6.0
 | 
			
		||||
androidx_recycler_version=1.2.1
 | 
			
		||||
appcompat_version=1.3.1
 | 
			
		||||
 | 
			
		||||
android_minSdkVersion=19
 | 
			
		||||
android_compileSdkVersion=31
 | 
			
		||||
android_buildToolsVersion=31.0.0
 | 
			
		||||
dexcount_version=3.0.0
 | 
			
		||||
junit_version=4.12
 | 
			
		||||
test_ext_junit_version=1.1.2
 | 
			
		||||
espresso_core=3.3.0
 | 
			
		||||
 | 
			
		||||
# JS NPM
 | 
			
		||||
 | 
			
		||||
crypto_js_version=4.1.1
 | 
			
		||||
 | 
			
		||||
# Dokka
 | 
			
		||||
 | 
			
		||||
dokka_version=1.5.30
 | 
			
		||||
 | 
			
		||||
# Project data
 | 
			
		||||
 | 
			
		||||
group=dev.inmo
 | 
			
		||||
version=0.5.31
 | 
			
		||||
android_code_version=72
 | 
			
		||||
version=0.9.19
 | 
			
		||||
android_code_version=109
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
[versions]
 | 
			
		||||
 | 
			
		||||
kt = "1.6.10"
 | 
			
		||||
kt-serialization = "1.3.2"
 | 
			
		||||
kt-coroutines = "1.6.1"
 | 
			
		||||
 | 
			
		||||
jb-compose = "1.1.1"
 | 
			
		||||
jb-exposed = "0.37.3"
 | 
			
		||||
jb-dokka = "1.6.10"
 | 
			
		||||
 | 
			
		||||
klock = "2.7.0"
 | 
			
		||||
uuid = "0.4.0"
 | 
			
		||||
 | 
			
		||||
ktor = "1.6.8"
 | 
			
		||||
 | 
			
		||||
gh-release = "2.2.12"
 | 
			
		||||
 | 
			
		||||
android-gradle = "7.0.4"
 | 
			
		||||
dexcount = "3.0.1"
 | 
			
		||||
 | 
			
		||||
android-coreKtx = "1.7.0"
 | 
			
		||||
android-recyclerView = "1.2.1"
 | 
			
		||||
android-appCompat = "1.4.1"
 | 
			
		||||
android-espresso = "3.3.0"
 | 
			
		||||
android-test = "1.1.2"
 | 
			
		||||
 | 
			
		||||
android-props-minSdk = "19"
 | 
			
		||||
android-props-compileSdk = "32"
 | 
			
		||||
android-props-buildTools = "32.0.0"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
 | 
			
		||||
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
 | 
			
		||||
 | 
			
		||||
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
 | 
			
		||||
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
 | 
			
		||||
 | 
			
		||||
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
 | 
			
		||||
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
 | 
			
		||||
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
 | 
			
		||||
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
 | 
			
		||||
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
 | 
			
		||||
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
 | 
			
		||||
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
 | 
			
		||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
 | 
			
		||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
 | 
			
		||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
 | 
			
		||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
 | 
			
		||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
 | 
			
		||||
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
 | 
			
		||||
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
 | 
			
		||||
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
 | 
			
		||||
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
 | 
			
		||||
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
 | 
			
		||||
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
 | 
			
		||||
 | 
			
		||||
[plugins]
 | 
			
		||||
 | 
			
		||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
 | 
			
		||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ kotlin {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api internalProject("micro_utils.ktor.common")
 | 
			
		||||
                api internalProject("micro_utils.coroutines")
 | 
			
		||||
                api "io.ktor:ktor-client-core:$ktor_version"
 | 
			
		||||
                api libs.ktor.client
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.features.websocket.ws
 | 
			
		||||
import io.ktor.client.request.HttpRequestBuilder
 | 
			
		||||
import io.ktor.http.cio.websocket.Frame
 | 
			
		||||
import io.ktor.http.cio.websocket.readBytes
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
@@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy
 | 
			
		||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
 | 
			
		||||
    url: String,
 | 
			
		||||
    crossinline checkReconnection: (Throwable?) -> Boolean = { true },
 | 
			
		||||
    noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    crossinline conversation: suspend (StandardKtorSerialInputData) -> T
 | 
			
		||||
): Flow<T> {
 | 
			
		||||
    val correctedUrl = url.asCorrectWebSocketUrl
 | 
			
		||||
@@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
 | 
			
		||||
        do {
 | 
			
		||||
            val reconnect = try {
 | 
			
		||||
                safely {
 | 
			
		||||
                    ws(correctedUrl) {
 | 
			
		||||
                    ws(correctedUrl, requestBuilder) {
 | 
			
		||||
                        for (received in incoming) {
 | 
			
		||||
                            when (received) {
 | 
			
		||||
                                is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
 | 
			
		||||
@@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
 | 
			
		||||
    url: String,
 | 
			
		||||
    crossinline checkReconnection: (Throwable?) -> Boolean = { true },
 | 
			
		||||
    deserializer: DeserializationStrategy<T>,
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
) = createStandardWebsocketFlow(
 | 
			
		||||
    url,
 | 
			
		||||
    checkReconnection
 | 
			
		||||
    checkReconnection,
 | 
			
		||||
    requestBuilder
 | 
			
		||||
) {
 | 
			
		||||
    serialFormat.decodeDefault(deserializer, it)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import io.ktor.client.request.forms.InputProvider
 | 
			
		||||
 | 
			
		||||
expect suspend fun MPPFile.inputProvider(): InputProvider
 | 
			
		||||
@@ -1,16 +1,20 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.common.filename
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.request.get
 | 
			
		||||
import io.ktor.client.request.post
 | 
			
		||||
import io.ktor.client.request.*
 | 
			
		||||
import io.ktor.client.request.forms.*
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import io.ktor.utils.io.core.ByteReadPacket
 | 
			
		||||
import kotlinx.serialization.*
 | 
			
		||||
 | 
			
		||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
 | 
			
		||||
 | 
			
		||||
class UnifiedRequester(
 | 
			
		||||
    private val client: HttpClient = HttpClient(),
 | 
			
		||||
    private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
    val client: HttpClient = HttpClient(),
 | 
			
		||||
    val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
) {
 | 
			
		||||
    suspend fun <ResultType> uniget(
 | 
			
		||||
        url: String,
 | 
			
		||||
@@ -31,11 +35,66 @@ class UnifiedRequester(
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>
 | 
			
		||||
    ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        filename: String,
 | 
			
		||||
        inputProvider: InputProvider,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <BodyType, ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        filename: String,
 | 
			
		||||
        inputProvider: InputProvider,
 | 
			
		||||
        otherData: BodyPair<BodyType>,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        mppFile: MPPFile,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {}
 | 
			
		||||
    ): ResultType = client.unimultipart(
 | 
			
		||||
        url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    suspend fun <BodyType, ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        mppFile: MPPFile,
 | 
			
		||||
        otherData: BodyPair<BodyType>,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {}
 | 
			
		||||
    ): ResultType = client.unimultipart(
 | 
			
		||||
        url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    fun <T> createStandardWebsocketFlow(
 | 
			
		||||
        url: String,
 | 
			
		||||
        checkReconnection: (Throwable?) -> Boolean = { true },
 | 
			
		||||
        deserializer: DeserializationStrategy<T>
 | 
			
		||||
    ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
 | 
			
		||||
        checkReconnection: (Throwable?) -> Boolean,
 | 
			
		||||
        deserializer: DeserializationStrategy<T>,
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
 | 
			
		||||
 | 
			
		||||
    fun <T> createStandardWebsocketFlow(
 | 
			
		||||
        url: String,
 | 
			
		||||
        deserializer: DeserializationStrategy<T>,
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ) = createStandardWebsocketFlow(url, { true  }, deserializer, requestBuilder)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val defaultRequester = UnifiedRequester()
 | 
			
		||||
@@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
 | 
			
		||||
}.let {
 | 
			
		||||
    serialFormat.decodeDefault(resultDeserializer, it)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    filename: String,
 | 
			
		||||
    inputProvider: InputProvider,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
 | 
			
		||||
    url,
 | 
			
		||||
    formData = formData {
 | 
			
		||||
        append(
 | 
			
		||||
            "bytes",
 | 
			
		||||
            inputProvider,
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, mimetype)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    }
 | 
			
		||||
) {
 | 
			
		||||
    requestBuilder()
 | 
			
		||||
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
 | 
			
		||||
 | 
			
		||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    filename: String,
 | 
			
		||||
    otherData: BodyPair<BodyType>,
 | 
			
		||||
    inputProvider: InputProvider,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    filename,
 | 
			
		||||
    inputProvider,
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder = {
 | 
			
		||||
        val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
 | 
			
		||||
        append(
 | 
			
		||||
            "data",
 | 
			
		||||
            InputProvider(serialized.size.toLong()) {
 | 
			
		||||
                ByteReadPacket(serialized)
 | 
			
		||||
            },
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=data.bytes")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    },
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    mppFile: MPPFile,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    mppFile.filename.string,
 | 
			
		||||
    mppFile.inputProvider(),
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder,
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    mppFile: MPPFile,
 | 
			
		||||
    otherData: BodyPair<BodyType>,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    mppFile,
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder = {
 | 
			
		||||
        val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
 | 
			
		||||
        append(
 | 
			
		||||
            "data",
 | 
			
		||||
            InputProvider(serialized.size.toLong()) {
 | 
			
		||||
                ByteReadPacket(serialized)
 | 
			
		||||
            },
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=data.bytes")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    },
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
 | 
			
		||||
expect suspend fun HttpClient.tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath: String,
 | 
			
		||||
    file: MPPFile,
 | 
			
		||||
    onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
 | 
			
		||||
): TemporalFileId
 | 
			
		||||
 | 
			
		||||
suspend fun UnifiedRequester.tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath: String,
 | 
			
		||||
    file: MPPFile,
 | 
			
		||||
    onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
 | 
			
		||||
): TemporalFileId = client.tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath, file, onUpload
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.*
 | 
			
		||||
import io.ktor.client.request.forms.InputProvider
 | 
			
		||||
import io.ktor.utils.io.core.ByteReadPacket
 | 
			
		||||
 | 
			
		||||
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
 | 
			
		||||
    InputProvider(it.size.toLong()) {
 | 
			
		||||
        ByteReadPacket(it)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import org.w3c.xhr.*
 | 
			
		||||
 | 
			
		||||
suspend fun tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath: String,
 | 
			
		||||
    file: MPPFile,
 | 
			
		||||
    onUpload: (Long, Long) -> Unit
 | 
			
		||||
): TemporalFileId {
 | 
			
		||||
    val formData = FormData()
 | 
			
		||||
    val answer = CompletableDeferred<TemporalFileId>()
 | 
			
		||||
 | 
			
		||||
    formData.append(
 | 
			
		||||
        "data",
 | 
			
		||||
        file
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val request = XMLHttpRequest()
 | 
			
		||||
    request.responseType = XMLHttpRequestResponseType.TEXT
 | 
			
		||||
    request.upload.onprogress = {
 | 
			
		||||
        onUpload(it.loaded.toLong(), it.total.toLong())
 | 
			
		||||
    }
 | 
			
		||||
    request.onload = {
 | 
			
		||||
        if (request.status == 200.toShort()) {
 | 
			
		||||
            answer.complete(TemporalFileId(request.responseText))
 | 
			
		||||
        } else {
 | 
			
		||||
            answer.completeExceptionally(Exception("Something went wrong: $it"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    request.onerror = {
 | 
			
		||||
        answer.completeExceptionally(Exception("Something went wrong: $it"))
 | 
			
		||||
    }
 | 
			
		||||
    request.open("POST", fullTempUploadDraftPath, true)
 | 
			
		||||
    request.send(formData)
 | 
			
		||||
 | 
			
		||||
    val handle = currentCoroutineContext().job.invokeOnCompletion {
 | 
			
		||||
        runCatching {
 | 
			
		||||
            request.abort()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return runCatching {
 | 
			
		||||
        answer.await()
 | 
			
		||||
    }.also {
 | 
			
		||||
        handle.dispose()
 | 
			
		||||
    }.getOrThrow()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
actual suspend fun HttpClient.tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath: String,
 | 
			
		||||
    file: MPPFile,
 | 
			
		||||
    onUpload: (uploaded: Long, count: Long) -> Unit
 | 
			
		||||
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import io.ktor.client.request.forms.InputProvider
 | 
			
		||||
import io.ktor.utils.io.streams.asInput
 | 
			
		||||
 | 
			
		||||
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
 | 
			
		||||
    inputStream().asInput()
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.common.filename
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.features.onUpload
 | 
			
		||||
import io.ktor.client.request.forms.formData
 | 
			
		||||
import io.ktor.client.request.forms.submitFormWithBinaryData
 | 
			
		||||
import io.ktor.http.Headers
 | 
			
		||||
import io.ktor.http.HttpHeaders
 | 
			
		||||
import java.net.URLConnection
 | 
			
		||||
 | 
			
		||||
internal val MPPFile.mimeType: String
 | 
			
		||||
    get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
 | 
			
		||||
 | 
			
		||||
actual suspend fun HttpClient.tempUpload(
 | 
			
		||||
    fullTempUploadDraftPath: String,
 | 
			
		||||
    file: MPPFile,
 | 
			
		||||
    onUpload: (Long, Long) -> Unit
 | 
			
		||||
): TemporalFileId {
 | 
			
		||||
    val inputProvider = file.inputProvider()
 | 
			
		||||
    val fileId = submitFormWithBinaryData<String>(
 | 
			
		||||
        fullTempUploadDraftPath,
 | 
			
		||||
        formData = formData {
 | 
			
		||||
            append(
 | 
			
		||||
                "data",
 | 
			
		||||
                inputProvider,
 | 
			
		||||
                Headers.build {
 | 
			
		||||
                    append(HttpHeaders.ContentType, file.mimeType)
 | 
			
		||||
                    append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        onUpload(onUpload)
 | 
			
		||||
    }
 | 
			
		||||
    return TemporalFileId(fileId)
 | 
			
		||||
}
 | 
			
		||||
@@ -10,8 +10,10 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
 | 
			
		||||
                api "com.soywiz.korlibs.klock:klock:$klockVersion"
 | 
			
		||||
                api internalProject("micro_utils.common")
 | 
			
		||||
                api libs.kt.serialization.cbor
 | 
			
		||||
                api libs.klock
 | 
			
		||||
                api libs.uuid
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.common
 | 
			
		||||
 | 
			
		||||
import kotlin.jvm.JvmInline
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
const val DefaultTemporalFilesSubPath = "temp_upload"
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
@JvmInline
 | 
			
		||||
value class TemporalFileId(val string: String)
 | 
			
		||||
@@ -16,10 +16,10 @@ kotlin {
 | 
			
		||||
 | 
			
		||||
        jvmMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "io.ktor:ktor-server:$ktor_version"
 | 
			
		||||
                api "io.ktor:ktor-server-cio:$ktor_version"
 | 
			
		||||
                api "io.ktor:ktor-server-host-common:$ktor_version"
 | 
			
		||||
                api "io.ktor:ktor-websockets:$ktor_version"
 | 
			
		||||
                api libs.ktor.server
 | 
			
		||||
                api libs.ktor.server.cio
 | 
			
		||||
                api libs.ktor.server.host.common
 | 
			
		||||
                api libs.ktor.websockets
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.safely
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.application.featureOrNull
 | 
			
		||||
import io.ktor.application.install
 | 
			
		||||
import io.ktor.http.URLProtocol
 | 
			
		||||
import io.ktor.http.cio.websocket.*
 | 
			
		||||
import io.ktor.routing.Route
 | 
			
		||||
import io.ktor.websocket.webSocket
 | 
			
		||||
import io.ktor.routing.application
 | 
			
		||||
import io.ktor.websocket.*
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collect
 | 
			
		||||
import kotlinx.serialization.SerializationStrategy
 | 
			
		||||
 | 
			
		||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
 | 
			
		||||
    if (incoming.tryReceive() != null) {
 | 
			
		||||
        close()
 | 
			
		||||
        throw CorrectCloseException
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun <T> Route.includeWebsocketHandling(
 | 
			
		||||
    suburl: String,
 | 
			
		||||
    flow: Flow<T>,
 | 
			
		||||
    converter: (T) -> StandardKtorSerialInputData
 | 
			
		||||
    protocol: URLProtocol = URLProtocol.WS,
 | 
			
		||||
    converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
 | 
			
		||||
) {
 | 
			
		||||
    webSocket(suburl) {
 | 
			
		||||
    application.apply {
 | 
			
		||||
        featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
 | 
			
		||||
    }
 | 
			
		||||
    webSocket(suburl, protocol.name) {
 | 
			
		||||
        safely {
 | 
			
		||||
            flow.collect {
 | 
			
		||||
                send(converter(it))
 | 
			
		||||
                converter(it) ?.let { data ->
 | 
			
		||||
                    send(data)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling(
 | 
			
		||||
    suburl: String,
 | 
			
		||||
    flow: Flow<T>,
 | 
			
		||||
    serializer: SerializationStrategy<T>,
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    protocol: URLProtocol = URLProtocol.WS,
 | 
			
		||||
    filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
 | 
			
		||||
) = includeWebsocketHandling(
 | 
			
		||||
    suburl,
 | 
			
		||||
    flow
 | 
			
		||||
) {
 | 
			
		||||
    flow,
 | 
			
		||||
    protocol,
 | 
			
		||||
    converter = if (filter == null) {
 | 
			
		||||
        {
 | 
			
		||||
            serialFormat.encodeDefault(serializer, it)
 | 
			
		||||
}
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        {
 | 
			
		||||
            if (filter(it)) {
 | 
			
		||||
                serialFormat.encodeDefault(serializer, it)
 | 
			
		||||
            } else {
 | 
			
		||||
                null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,39 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.server
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.*
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.safely
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.application.ApplicationCall
 | 
			
		||||
import io.ktor.application.call
 | 
			
		||||
import io.ktor.http.ContentType
 | 
			
		||||
import io.ktor.http.HttpStatusCode
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import io.ktor.http.content.PartData
 | 
			
		||||
import io.ktor.http.content.forEachPart
 | 
			
		||||
import io.ktor.request.receive
 | 
			
		||||
import io.ktor.request.receiveMultipart
 | 
			
		||||
import io.ktor.response.respond
 | 
			
		||||
import io.ktor.response.respondBytes
 | 
			
		||||
import io.ktor.routing.Route
 | 
			
		||||
import io.ktor.util.asStream
 | 
			
		||||
import io.ktor.util.cio.writeChannel
 | 
			
		||||
import io.ktor.util.pipeline.PipelineContext
 | 
			
		||||
import io.ktor.utils.io.core.*
 | 
			
		||||
import io.ktor.websocket.WebSocketServerSession
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.serialization.*
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.File.createTempFile
 | 
			
		||||
 | 
			
		||||
class UnifiedRouter(
 | 
			
		||||
    private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
 | 
			
		||||
    val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
 | 
			
		||||
) {
 | 
			
		||||
    fun <T> Route.includeWebsocketHandling(
 | 
			
		||||
        suburl: String,
 | 
			
		||||
        flow: Flow<T>,
 | 
			
		||||
        serializer: SerializationStrategy<T>
 | 
			
		||||
    ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
 | 
			
		||||
        serializer: SerializationStrategy<T>,
 | 
			
		||||
        protocol: URLProtocol = URLProtocol.WS,
 | 
			
		||||
        filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
 | 
			
		||||
    ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
 | 
			
		||||
 | 
			
		||||
    suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
 | 
			
		||||
        answerSerializer: SerializationStrategy<T>,
 | 
			
		||||
@@ -81,6 +92,11 @@ class UnifiedRouter(
 | 
			
		||||
            call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val default
 | 
			
		||||
            get() = defaultUnifiedRouter
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val defaultUnifiedRouter = UnifiedRouter()
 | 
			
		||||
@@ -104,6 +120,139 @@ suspend fun <T> ApplicationCall.uniload(
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.uniloadMultipart(
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {}
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: Input? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                when (it.name) {
 | 
			
		||||
                    "bytes" -> resultInput = it.provider()
 | 
			
		||||
                    else -> onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resultInput ?: error("Bytes has not been received")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> ApplicationCall.uniloadMultipart(
 | 
			
		||||
    deserializer: DeserializationStrategy<T>,
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {}
 | 
			
		||||
): Pair<Input, T> {
 | 
			
		||||
    var data: Optional<T>? = null
 | 
			
		||||
    val resultInput = uniloadMultipart(
 | 
			
		||||
        onFormItem,
 | 
			
		||||
        {
 | 
			
		||||
            if (it.name == "data") {
 | 
			
		||||
                data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
 | 
			
		||||
            } else {
 | 
			
		||||
                onCustomFileItem(it)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        onBinaryContent
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val completeData = data ?: error("Data has not been received")
 | 
			
		||||
    return resultInput to (completeData.dataOrNull().let { it as T })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> ApplicationCall.uniloadMultipartFile(
 | 
			
		||||
    deserializer: DeserializationStrategy<T>,
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {},
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: MPPFile? = null
 | 
			
		||||
    var data: Optional<T>? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                when (it.name) {
 | 
			
		||||
                    "bytes" -> {
 | 
			
		||||
                        val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
 | 
			
		||||
                        resultInput = MPPFile.createTempFile(
 | 
			
		||||
                            name.nameWithoutExtension.let {
 | 
			
		||||
                                var resultName = it
 | 
			
		||||
                                while (resultName.length < 3) {
 | 
			
		||||
                                    resultName += "_"
 | 
			
		||||
                                }
 | 
			
		||||
                                resultName
 | 
			
		||||
                            },
 | 
			
		||||
                            ".${name.extension}"
 | 
			
		||||
                        ).apply {
 | 
			
		||||
                            outputStream().use { fileStream ->
 | 
			
		||||
                                it.provider().asStream().copyTo(fileStream)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    "data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
 | 
			
		||||
                    else -> onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val completeData = data ?: error("Data has not been received")
 | 
			
		||||
    (resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.uniloadMultipartFile(
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {},
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: MPPFile? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                if (it.name == "bytes") {
 | 
			
		||||
                    val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
 | 
			
		||||
                    resultInput = MPPFile.createTempFile(
 | 
			
		||||
                        name.nameWithoutExtension.let {
 | 
			
		||||
                            var resultName = it
 | 
			
		||||
                            while (resultName.length < 3) {
 | 
			
		||||
                                resultName += "_"
 | 
			
		||||
                            }
 | 
			
		||||
                            resultName
 | 
			
		||||
                        },
 | 
			
		||||
                        ".${name.extension}"
 | 
			
		||||
                    ).apply {
 | 
			
		||||
                        outputStream().use { fileStream ->
 | 
			
		||||
                            it.provider().asStream().copyTo(fileStream)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resultInput ?: error("Bytes has not been received")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.getParameterOrSendError(
 | 
			
		||||
    field: String
 | 
			
		||||
) = parameters[field].also {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.server
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
 | 
			
		||||
import io.ktor.application.Application
 | 
			
		||||
import io.ktor.server.cio.CIO
 | 
			
		||||
import io.ktor.server.engine.*
 | 
			
		||||
@@ -31,3 +32,27 @@ fun createKtorServer(
 | 
			
		||||
    port: Int = Random.nextInt(1024, 65535),
 | 
			
		||||
    block: Application.() -> Unit
 | 
			
		||||
): ApplicationEngine = createKtorServer(CIO, host, port, block)
 | 
			
		||||
 | 
			
		||||
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
 | 
			
		||||
    engine: ApplicationEngineFactory<TEngine, TConfiguration>,
 | 
			
		||||
    host: String = "localhost",
 | 
			
		||||
    port: Int = Random.nextInt(1024, 65535),
 | 
			
		||||
    configurators: List<KtorApplicationConfigurator>
 | 
			
		||||
): TEngine = createKtorServer(
 | 
			
		||||
    engine,
 | 
			
		||||
    host,
 | 
			
		||||
    port
 | 
			
		||||
) {
 | 
			
		||||
    configurators.forEach { it.apply { configure() } }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create server with [CIO] server engine without starting of it
 | 
			
		||||
 *
 | 
			
		||||
 * @see ApplicationEngine.start
 | 
			
		||||
 */
 | 
			
		||||
fun createKtorServer(
 | 
			
		||||
    host: String = "localhost",
 | 
			
		||||
    port: Int = Random.nextInt(1024, 65535),
 | 
			
		||||
    configurators: List<KtorApplicationConfigurator>
 | 
			
		||||
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,132 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.server
 | 
			
		||||
 | 
			
		||||
import com.benasher44.uuid.uuid4
 | 
			
		||||
import dev.inmo.micro_utils.common.FileName
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
 | 
			
		||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
 | 
			
		||||
import io.ktor.application.call
 | 
			
		||||
import io.ktor.http.HttpStatusCode
 | 
			
		||||
import io.ktor.http.content.PartData
 | 
			
		||||
import io.ktor.http.content.streamProvider
 | 
			
		||||
import io.ktor.request.receiveMultipart
 | 
			
		||||
import io.ktor.response.respond
 | 
			
		||||
import io.ktor.routing.Route
 | 
			
		||||
import io.ktor.routing.post
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.nio.file.Files
 | 
			
		||||
import java.nio.file.attribute.FileTime
 | 
			
		||||
 | 
			
		||||
class TemporalFilesRoutingConfigurator(
 | 
			
		||||
    private val subpath: String = DefaultTemporalFilesSubPath,
 | 
			
		||||
    private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
 | 
			
		||||
    private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
 | 
			
		||||
) : ApplicationRoutingConfigurator.Element {
 | 
			
		||||
    interface TemporalFilesUtilizer {
 | 
			
		||||
        fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
 | 
			
		||||
 | 
			
		||||
        companion object : TemporalFilesUtilizer {
 | 
			
		||||
            class ByTimerUtilizer(
 | 
			
		||||
                private val removeMillis: Long,
 | 
			
		||||
                private val scope: CoroutineScope
 | 
			
		||||
            ) : TemporalFilesUtilizer {
 | 
			
		||||
                override fun start(
 | 
			
		||||
                    filesMap: MutableMap<TemporalFileId, MPPFile>,
 | 
			
		||||
                    filesMutex: Mutex,
 | 
			
		||||
                    onNewFileFlow: Flow<TemporalFileId>
 | 
			
		||||
                ): Job = scope.launchSafelyWithoutExceptions {
 | 
			
		||||
                    while (isActive) {
 | 
			
		||||
                        val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
 | 
			
		||||
                            fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
 | 
			
		||||
                        }
 | 
			
		||||
                        if (filesWithCreationInfo.isEmpty()) {
 | 
			
		||||
                            delay(removeMillis)
 | 
			
		||||
                            continue
 | 
			
		||||
                        }
 | 
			
		||||
                        var min = filesWithCreationInfo.first()
 | 
			
		||||
                        for (fileWithCreationInfo in filesWithCreationInfo) {
 | 
			
		||||
                            if (fileWithCreationInfo.second < min.second) {
 | 
			
		||||
                                min = fileWithCreationInfo
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        delay(System.currentTimeMillis() - (min.second + removeMillis))
 | 
			
		||||
                        filesMutex.withLock {
 | 
			
		||||
                            filesMap.remove(min.first)
 | 
			
		||||
                        } ?.delete()
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun start(
 | 
			
		||||
                filesMap: MutableMap<TemporalFileId, MPPFile>,
 | 
			
		||||
                filesMutex: Mutex,
 | 
			
		||||
                onNewFileFlow: Flow<TemporalFileId>
 | 
			
		||||
            ): Job = Job()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
 | 
			
		||||
    private val temporalFilesMutex = Mutex()
 | 
			
		||||
    private val filesFlow = MutableSharedFlow<TemporalFileId>()
 | 
			
		||||
    val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
 | 
			
		||||
 | 
			
		||||
    override fun Route.invoke() {
 | 
			
		||||
        post(subpath) {
 | 
			
		||||
            unifiedRouter.apply {
 | 
			
		||||
                val multipart = call.receiveMultipart()
 | 
			
		||||
 | 
			
		||||
                var fileInfo: Pair<TemporalFileId, MPPFile>? = null
 | 
			
		||||
                var part = multipart.readPart()
 | 
			
		||||
 | 
			
		||||
                while (part != null) {
 | 
			
		||||
                    if (part is PartData.FileItem) {
 | 
			
		||||
                        break
 | 
			
		||||
                    }
 | 
			
		||||
                    part = multipart.readPart()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                part ?.let {
 | 
			
		||||
                    if (it is PartData.FileItem) {
 | 
			
		||||
                        val fileId = TemporalFileId(uuid4().toString())
 | 
			
		||||
                        val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
 | 
			
		||||
                        fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
 | 
			
		||||
                            outputStream().use { outputStream ->
 | 
			
		||||
                                it.streamProvider().use {
 | 
			
		||||
                                    it.copyTo(outputStream)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            deleteOnExit()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                fileInfo ?.also { (fileId, file) ->
 | 
			
		||||
                    temporalFilesMutex.withLock {
 | 
			
		||||
                        temporalFilesMap[fileId] = file
 | 
			
		||||
                    }
 | 
			
		||||
                    call.respond(fileId.string)
 | 
			
		||||
                    launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
 | 
			
		||||
                } ?: call.respond(HttpStatusCode.BadRequest)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
 | 
			
		||||
        temporalFilesMutex.withLock {
 | 
			
		||||
            temporalFilesMap.remove(temporalFileId)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
 | 
			
		||||
 | 
			
		||||
    suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
 | 
			
		||||
        temporalFilesMap.remove(temporalFileId)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,8 +4,8 @@ buildscript {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
 | 
			
		||||
        classpath libs.buildscript.kt.gradle
 | 
			
		||||
        classpath libs.buildscript.kt.serialization
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -16,11 +16,16 @@ plugins {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 | 
			
		||||
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
 | 
			
		||||
    implementation libs.kt.stdlib
 | 
			
		||||
    implementation libs.kt.serialization
 | 
			
		||||
 | 
			
		||||
    implementation "io.ktor:ktor-client-core:$ktor_version"
 | 
			
		||||
    implementation "io.ktor:ktor-client-java:$ktor_version"
 | 
			
		||||
    implementation libs.ktor.client
 | 
			
		||||
    implementation libs.ktor.client.java
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mainClassName="MainKt"
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    sourceCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
    targetCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
 | 
			
		||||
 | 
			
		||||
package dev.inmo.micro_utils.language_codes
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import requests
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
import pandas as pd
 | 
			
		||||
import itertools
 | 
			
		||||
 | 
			
		||||
def fix_name(category, raw_name):
 | 
			
		||||
    splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+')
 | 
			
		||||
    out1 = ""
 | 
			
		||||
    for s in splitted:
 | 
			
		||||
        out1 += s.capitalize()
 | 
			
		||||
 | 
			
		||||
    result = ""
 | 
			
		||||
    if out1[0].isdigit():
 | 
			
		||||
        result += category[0].capitalize()
 | 
			
		||||
        result += out1
 | 
			
		||||
    else:
 | 
			
		||||
        result += out1
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    df = pd.read_html(open('table.html', 'r'))
 | 
			
		||||
    mimes = []
 | 
			
		||||
    for row in df[0].iterrows():
 | 
			
		||||
        mime = row[1][1]
 | 
			
		||||
        mime_category = mime.split('/', 1)[0]
 | 
			
		||||
        mime_name = mime.split('/', 1)[1]
 | 
			
		||||
        mimes.append({
 | 
			
		||||
            'mime_category': mime_category,
 | 
			
		||||
            'mime_name': mime_name,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    # codegen
 | 
			
		||||
 | 
			
		||||
    mimes.sort(key=lambda x: x['mime_category'])
 | 
			
		||||
    grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
 | 
			
		||||
    code = ''
 | 
			
		||||
    code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
 | 
			
		||||
    code2 += '    KnownMimeTypes.Any,\n'
 | 
			
		||||
    for key, group in grouped:
 | 
			
		||||
        group_name = key.capitalize()
 | 
			
		||||
        code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name
 | 
			
		||||
        code += '    @Serializable(MimeTypeSerializer::class)\n    object Any: %s ("%s/*")\n' % (group_name, key)
 | 
			
		||||
        for mime in group:
 | 
			
		||||
            name = fix_name(mime['mime_category'], mime['mime_name'])
 | 
			
		||||
            code += '    @Serializable(MimeTypeSerializer::class)\n    object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
 | 
			
		||||
            code2 += '    KnownMimeTypes.%s.%s,\n' % (group_name, name)
 | 
			
		||||
        code += '}\n\n'
 | 
			
		||||
    code2 += ')\n'
 | 
			
		||||
    with open('out1.txt', 'w') as file:
 | 
			
		||||
        file.write(code)
 | 
			
		||||
    with open('out2.txt', 'w') as file:
 | 
			
		||||
        file.write(code2)
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
 | 
			
		||||
 | 
			
		||||
package dev.inmo.micro_utils.mime_types
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,6 @@ kotlin {
 | 
			
		||||
apply from: "$defaultAndroidSettingsPresetPath"
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    toolchain {
 | 
			
		||||
        languageVersion = JavaLanguageVersion.of(8)
 | 
			
		||||
    }
 | 
			
		||||
    sourceCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
    targetCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,13 @@ project.group = "$group"
 | 
			
		||||
apply from: "$publishGradlePath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    jvm()
 | 
			
		||||
    jvm {
 | 
			
		||||
        compilations.main {
 | 
			
		||||
            kotlinOptions {
 | 
			
		||||
                jvmTarget = "1.8"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
@@ -28,7 +34,6 @@ kotlin {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    toolchain {
 | 
			
		||||
        languageVersion = JavaLanguageVersion.of(8)
 | 
			
		||||
    }
 | 
			
		||||
    sourceCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
    targetCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,13 @@ project.group = "$group"
 | 
			
		||||
apply from: "$publishGradlePath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    jvm()
 | 
			
		||||
    jvm {
 | 
			
		||||
        compilations.main {
 | 
			
		||||
            kotlinOptions {
 | 
			
		||||
                jvmTarget = "1.8"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    js (IR) {
 | 
			
		||||
        browser()
 | 
			
		||||
        nodejs()
 | 
			
		||||
@@ -17,7 +23,7 @@ kotlin {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('stdlib')
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
 | 
			
		||||
                implementation libs.kt.serialization
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        commonTest {
 | 
			
		||||
@@ -40,8 +46,8 @@ kotlin {
 | 
			
		||||
        androidTest {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('test-junit')
 | 
			
		||||
                implementation "androidx.test.ext:junit:$test_ext_junit_version"
 | 
			
		||||
                implementation "androidx.test.espresso:espresso-core:$espresso_core"
 | 
			
		||||
                implementation libs.android.test.junit
 | 
			
		||||
                implementation libs.android.espresso
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -50,7 +56,6 @@ kotlin {
 | 
			
		||||
apply from: "$defaultAndroidSettingsPresetPath"
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    toolchain {
 | 
			
		||||
        languageVersion = JavaLanguageVersion.of(8)
 | 
			
		||||
    }
 | 
			
		||||
    sourceCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
    targetCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								mppProjectWithSerializationAndCompose.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								mppProjectWithSerializationAndCompose.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
project.version = "$version"
 | 
			
		||||
project.group = "$group"
 | 
			
		||||
 | 
			
		||||
apply from: "$publishGradlePath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    jvm {
 | 
			
		||||
        compilations.main {
 | 
			
		||||
            kotlinOptions {
 | 
			
		||||
                jvmTarget = "1.8"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    js (IR) {
 | 
			
		||||
        browser()
 | 
			
		||||
        nodejs()
 | 
			
		||||
    }
 | 
			
		||||
    android {
 | 
			
		||||
        publishAllLibraryVariants()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('stdlib')
 | 
			
		||||
                implementation libs.kt.serialization
 | 
			
		||||
                implementation compose.runtime
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        commonTest {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('test-common')
 | 
			
		||||
                implementation kotlin('test-annotations-common')
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jvmMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation compose.desktop.currentOs
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jvmTest {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('test-junit')
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jsMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation compose.web.core
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        jsTest {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('test-js')
 | 
			
		||||
                implementation kotlin('test-junit')
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        androidTest {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation kotlin('test-junit')
 | 
			
		||||
                implementation libs.android.test.junit
 | 
			
		||||
                implementation libs.android.espresso
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$defaultAndroidSettingsPresetPath"
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    sourceCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
    targetCompatibility = JavaVersion.VERSION_1_8
 | 
			
		||||
}
 | 
			
		||||
@@ -5,3 +5,13 @@ plugins {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$mppProjectWithSerializationPresetPath"
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.pagination
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.intersect
 | 
			
		||||
import kotlin.math.ceil
 | 
			
		||||
import kotlin.math.floor
 | 
			
		||||
 | 
			
		||||
@@ -9,7 +10,7 @@ import kotlin.math.floor
 | 
			
		||||
 * If you want to request something, you should use [SimplePagination]. If you need to return some result including
 | 
			
		||||
 * pagination - [PaginationResult]
 | 
			
		||||
 */
 | 
			
		||||
interface Pagination {
 | 
			
		||||
interface Pagination : ClosedRange<Int> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Started with 0.
 | 
			
		||||
     * Number of page inside of pagination. Offset can be calculated as [page] * [size]
 | 
			
		||||
@@ -20,6 +21,17 @@ interface Pagination {
 | 
			
		||||
     * Size of current page. Offset can be calculated as [page] * [size]
 | 
			
		||||
     */
 | 
			
		||||
    val size: Int
 | 
			
		||||
 | 
			
		||||
    override val start: Int
 | 
			
		||||
        get() = page * size
 | 
			
		||||
    override val endInclusive: Int
 | 
			
		||||
        get() = start + size - 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Pagination.intersect(
 | 
			
		||||
    other: Pagination
 | 
			
		||||
): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
 | 
			
		||||
    PaginationByIndexes(it.first, it.second)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -32,7 +44,7 @@ inline val Pagination.isFirstPage
 | 
			
		||||
 * First number in index of objects. It can be used as offset for databases or other data sources
 | 
			
		||||
 */
 | 
			
		||||
val Pagination.firstIndex: Int
 | 
			
		||||
    get() = page * size
 | 
			
		||||
    get() = start
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Last number in index of objects. In fact, one [Pagination] object represent data in next range:
 | 
			
		||||
@@ -41,7 +53,7 @@ val Pagination.firstIndex: Int
 | 
			
		||||
 * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
 | 
			
		||||
 */
 | 
			
		||||
val Pagination.lastIndexExclusive: Int
 | 
			
		||||
    get() = firstIndex + size
 | 
			
		||||
    get() = endInclusive + 1
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Last number in index of objects. In fact, one [Pagination] object represent data in next range:
 | 
			
		||||
@@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int
 | 
			
		||||
 * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
 | 
			
		||||
 */
 | 
			
		||||
val Pagination.lastIndex: Int
 | 
			
		||||
    get() = lastIndexExclusive - 1
 | 
			
		||||
    get() = endInclusive
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculates pages count for given [datasetSize]
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,16 @@ suspend fun <T> getAll(
 | 
			
		||||
    return results.toList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T, R> R.getAllBy(
 | 
			
		||||
    initialPagination: Pagination = FirstPagePagination(),
 | 
			
		||||
    paginationMapper: R.(PaginationResult<T>) -> Pagination?,
 | 
			
		||||
    block: suspend R.(Pagination) -> PaginationResult<T>
 | 
			
		||||
): List<T> = getAll(
 | 
			
		||||
    initialPagination,
 | 
			
		||||
    { paginationMapper(it) },
 | 
			
		||||
    { block(it) }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <T> getAllWithNextPaging(
 | 
			
		||||
    initialPagination: Pagination = FirstPagePagination(),
 | 
			
		||||
    block: suspend (Pagination) -> PaginationResult<T>
 | 
			
		||||
@@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging(
 | 
			
		||||
    block
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <T, R> R.getAllByWithNextPaging(
 | 
			
		||||
    initialPagination: Pagination = FirstPagePagination(),
 | 
			
		||||
    block: suspend R.(Pagination) -> PaginationResult<T>
 | 
			
		||||
): List<T> = getAllWithNextPaging(
 | 
			
		||||
    initialPagination,
 | 
			
		||||
    { block(it) }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <T> getAllWithCurrentPaging(
 | 
			
		||||
    initialPagination: Pagination = FirstPagePagination(),
 | 
			
		||||
    block: suspend (Pagination) -> PaginationResult<T>
 | 
			
		||||
@@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging(
 | 
			
		||||
    { it.currentPageIfNotEmpty() },
 | 
			
		||||
    block
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <T, R> R.getAllByWithCurrentPaging(
 | 
			
		||||
    initialPagination: Pagination = FirstPagePagination(),
 | 
			
		||||
    block: suspend R.(Pagination) -> PaginationResult<T>
 | 
			
		||||
): List<T> = getAllWithCurrentPaging(
 | 
			
		||||
    initialPagination,
 | 
			
		||||
    { block(it) }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
 | 
			
		||||
        size.toLong()
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
 | 
			
		||||
    is List<T> -> optionallyReverse(reverse)
 | 
			
		||||
    is Set<T> -> optionallyReverse(reverse)
 | 
			
		||||
    else -> if (reverse) {
 | 
			
		||||
        reversed()
 | 
			
		||||
    } else {
 | 
			
		||||
        this
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
 | 
			
		||||
    reversed()
 | 
			
		||||
} else {
 | 
			
		||||
    this
 | 
			
		||||
}
 | 
			
		||||
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
 | 
			
		||||
    reversed().toSet()
 | 
			
		||||
} else {
 | 
			
		||||
    this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
 | 
			
		||||
    Array(size) {
 | 
			
		||||
        get(lastIndex - it)
 | 
			
		||||
    }
 | 
			
		||||
} else {
 | 
			
		||||
    this
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
 | 
			
		||||
 * Shortcut for [reverse]
 | 
			
		||||
 */
 | 
			
		||||
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
 | 
			
		||||
 | 
			
		||||
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
 | 
			
		||||
    reverse(objectsCount)
 | 
			
		||||
} else {
 | 
			
		||||
    this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
 | 
			
		||||
    reverse(objectsCount)
 | 
			
		||||
} else {
 | 
			
		||||
    this
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ kotlin {
 | 
			
		||||
        }
 | 
			
		||||
        jvmMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
 | 
			
		||||
                api libs.jb.exposed
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,8 @@ kotlin {
 | 
			
		||||
 | 
			
		||||
        jvmMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "io.ktor:ktor-server:$ktor_version"
 | 
			
		||||
                api "io.ktor:ktor-server-host-common:$ktor_version"
 | 
			
		||||
                api libs.ktor.server
 | 
			
		||||
                api libs.ktor.server.host.common
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
apply plugin: 'maven-publish'
 | 
			
		||||
apply plugin: 'signing'
 | 
			
		||||
 | 
			
		||||
task javadocsJar(type: Jar) {
 | 
			
		||||
    classifier = 'javadoc'
 | 
			
		||||
@@ -70,7 +69,18 @@ publishing {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
signing {
 | 
			
		||||
if (project.hasProperty("signing.gnupg.keyName")) {
 | 
			
		||||
    apply plugin: 'signing'
 | 
			
		||||
    
 | 
			
		||||
    signing {
 | 
			
		||||
        useGpgCmd()
 | 
			
		||||
    
 | 
			
		||||
        sign publishing.publications
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    task signAll {
 | 
			
		||||
        tasks.withType(Sign).forEach {
 | 
			
		||||
            dependsOn(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
 | 
			
		||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
 | 
			
		||||
@@ -10,10 +10,10 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
 | 
			
		||||
                api libs.kt.coroutines
 | 
			
		||||
                api internalProject("micro_utils.pagination.common")
 | 
			
		||||
 | 
			
		||||
                api "com.benasher44:uuid:$uuidVersion"
 | 
			
		||||
                api libs.uuid
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +24,7 @@ kotlin {
 | 
			
		||||
        }
 | 
			
		||||
        androidMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api "androidx.core:core-ktx:$core_ktx_version"
 | 
			
		||||
                api libs.android.coreKtx
 | 
			
		||||
                api internalProject("micro_utils.common")
 | 
			
		||||
                api internalProject("micro_utils.coroutines")
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo {
 | 
			
		||||
     * By default, instance of this interface will check that version of table with name [tableName] is less than
 | 
			
		||||
     * [version] or is absent
 | 
			
		||||
     *
 | 
			
		||||
     * * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
 | 
			
		||||
     * * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
 | 
			
		||||
     * until database version will be equal to [version]
 | 
			
		||||
     * In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
 | 
			
		||||
     * than parameter [version] or null, it will increase version one-by-one until database version will be equal to
 | 
			
		||||
     * [version]
 | 
			
		||||
     *
 | 
			
		||||
     * @param version Current version of table
 | 
			
		||||
     * @param onCreate This callback will be called in case when table have no information about table
 | 
			
		||||
     * @param onCreate This callback will be called in case when repo have no information about table
 | 
			
		||||
     * @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
 | 
			
		||||
     * "to" will always be greater than "from"
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,15 @@ import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
 | 
			
		||||
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
    helper: StandardSQLHelper
 | 
			
		||||
    helper: StandardSQLHelper,
 | 
			
		||||
    replyInFlows: Int = 0,
 | 
			
		||||
    extraBufferCapacityInFlows: Int = 64
 | 
			
		||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
 | 
			
		||||
    AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
 | 
			
		||||
    StandardCRUDRepo<ObjectType, IdType, InputValueType> {
 | 
			
		||||
    protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
 | 
			
		||||
    protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
 | 
			
		||||
    protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
 | 
			
		||||
    protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
 | 
			
		||||
    protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
 | 
			
		||||
    protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows)
 | 
			
		||||
    override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
 | 
			
		||||
 
 | 
			
		||||
@@ -10,15 +10,16 @@ import org.jetbrains.exposed.sql.transactions.transaction
 | 
			
		||||
 | 
			
		||||
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
    flowsChannelsSize: Int = 0,
 | 
			
		||||
    tableName: String = ""
 | 
			
		||||
    tableName: String = "",
 | 
			
		||||
    replyCacheInFlows: Int = 0
 | 
			
		||||
) :
 | 
			
		||||
    AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName),
 | 
			
		||||
    ExposedCRUDRepo<ObjectType, IdType>,
 | 
			
		||||
    WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
 | 
			
		||||
{
 | 
			
		||||
    protected val newObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
 | 
			
		||||
    protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
 | 
			
		||||
    protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(flowsChannelsSize)
 | 
			
		||||
    protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
 | 
			
		||||
    protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
 | 
			
		||||
    protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
 | 
			
		||||
 | 
			
		||||
    override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>(
 | 
			
		||||
    valueColumnAllocator,
 | 
			
		||||
    tableName
 | 
			
		||||
) {
 | 
			
		||||
    private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
 | 
			
		||||
    private val _onValueRemoved = MutableSharedFlow<Key>()
 | 
			
		||||
    protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
 | 
			
		||||
    protected val _onValueRemoved = MutableSharedFlow<Key>()
 | 
			
		||||
 | 
			
		||||
    override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
 | 
			
		||||
    override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
 | 
			
		||||
    valueColumnAllocator: ColumnAllocator<Value>,
 | 
			
		||||
    tableName: String? = null
 | 
			
		||||
) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
 | 
			
		||||
    protected val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    protected val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
    val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
    override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
 | 
			
		||||
 | 
			
		||||
    init { initTable() }
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user