Compare commits

..

11 Commits

8 changed files with 276 additions and 141 deletions

View File

@@ -1,5 +1,19 @@
# Changelog # Changelog
## 0.11.4
* `Coroutines`:
* `Compose`:
* Add extension `StateFlow#asMutableComposeListState` and `StateFlow#asComposeList`
* Add extension `StateFlow#asMutableComposeState`/`StateFlow#asComposeState`
## 0.11.3
* `Ktor`:
* Support of `WebSockets` has been improved
* `Client`:
* New extensions: `HttpClient#openBaseWebSocketFlow`, `HttpClient#openWebSocketFlow`, `HttpClient#openSecureWebSocketFlow`
## 0.11.2 ## 0.11.2
* `Ktor`: * `Ktor`:

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.inmo.micro_utils.common.applyDiff
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
scope: CoroutineScope
): SnapshotStateList<T> {
val state = mutableStateListOf<T>()
subscribeSafelyWithoutExceptions(scope) {
state.applyDiff(it)
}
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asComposeList(
scope: CoroutineScope
): List<T> = asMutableComposeListState(scope)

View File

@@ -0,0 +1,35 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
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>.asMutableComposeState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asMutableComposeState(
scope: CoroutineScope
): MutableState<T> = asMutableComposeState(value, scope)
fun <T> Flow<T>.asComposeState(
initial: T,
scope: CoroutineScope
): State<T> {
val state = asMutableComposeState(initial, scope)
return derivedStateOf { state.value }
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asComposeState(
scope: CoroutineScope
): State<T> = asComposeState(value, scope)

View File

@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.11.2 version=0.11.4
android_code_version=126 android_code_version=128

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.Warning
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
@@ -7,35 +8,25 @@ import io.ktor.client.HttpClient
import io.ktor.client.plugins.pluginOrNull import io.ktor.client.plugins.pluginOrNull
import io.ktor.client.plugins.websocket.* import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.websocket.Frame import io.ktor.http.URLProtocol
import io.ktor.websocket.readBytes import kotlinx.coroutines.channels.SendChannel
import io.ktor.websocket.serialization.sendSerializedBase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.serialization.DeserializationStrategy
/** /**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish * @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting * connection. Must return true in case if must be reconnected. By default always reconnecting
*/ */
inline fun <reified T : Any> HttpClient.createStandardWebsocketFlow( @Warning("This feature is internal and should not be used directly. It is can be changed without any notification and warranty on compile-time or other guaranties")
url: String, inline fun <reified T : Any> openBaseWebSocketFlow(
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true }, noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {} noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit
): Flow<T> { ): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
val correctedUrl = url.asCorrectWebSocketUrl
return channelFlow { return channelFlow {
do { do {
val reconnect = runCatchingSafely { val reconnect = runCatchingSafely {
ws(correctedUrl, requestBuilder) { webSocketSessionRequest()
while (isActive) {
send(receiveDeserialized<T>())
}
}
checkReconnection(null) checkReconnection(null)
}.getOrElse { e -> }.getOrElse { e ->
checkReconnection(e).also { checkReconnection(e).also {
@@ -53,3 +44,60 @@ inline fun <reified T : Any> HttpClient.createStandardWebsocketFlow(
} }
} }
} }
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
url: String,
useSecureConnection: Boolean,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
return openBaseWebSocketFlow<T>(checkReconnection) {
val block: suspend DefaultClientWebSocketSession.() -> Unit = {
while (isActive) {
send(receiveDeserialized<T>())
}
}
if (useSecureConnection) {
wss(url, requestBuilder, block)
} else {
ws(url, requestBuilder, block)
}
}
}
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, false, checkReconnection, requestBuilder)
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openSecureWebSocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, true, checkReconnection, requestBuilder)
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.createStandardWebsocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, checkReconnection, requestBuilder)

View File

@@ -4,7 +4,10 @@ import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.w3c.dom.mediasource.ENDED
import org.w3c.dom.mediasource.ReadyState
import org.w3c.xhr.* import org.w3c.xhr.*
import org.w3c.xhr.XMLHttpRequest.Companion.DONE
suspend fun tempUpload( suspend fun tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
@@ -12,7 +15,7 @@ suspend fun tempUpload(
onUpload: (Long, Long) -> Unit onUpload: (Long, Long) -> Unit
): TemporalFileId { ): TemporalFileId {
val formData = FormData() val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>() val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job)
formData.append( formData.append(
"data", "data",
@@ -37,17 +40,15 @@ suspend fun tempUpload(
request.open("POST", fullTempUploadDraftPath, true) request.open("POST", fullTempUploadDraftPath, true)
request.send(formData) request.send(formData)
val handle = currentCoroutineContext().job.invokeOnCompletion { answer.invokeOnCompletion {
runCatching { runCatching {
if (request.readyState != DONE) {
request.abort() request.abort()
} }
} }
}
return runCatching { return answer.await()
answer.await()
}.also {
handle.dispose()
}.getOrThrow()
} }

View File

@@ -1,7 +1,7 @@
swagger: "2.0" openapi: "3.0.0"
info: info:
description: "This is a template for the CRUD repositories from [microutils](https://github.com/InsanusMokrassar/MicroUtils/tree/master/repos/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/ktor/server/crud)" description: "This is a template for the CRUD repositories from [microutils](https://github.com/InsanusMokrassar/MicroUtils/tree/master/repos/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/ktor/server/crud)"
version: "0.11.0" version: "0.11.3"
title: "CRUD Repo" title: "CRUD Repo"
contact: contact:
email: "ovsyannikov.alexey95@gmail.com" email: "ovsyannikov.alexey95@gmail.com"
@@ -11,61 +11,35 @@ tags:
- name: "Write" - name: "Write"
description: "Operations with `post` request in most cases" description: "Operations with `post` request in most cases"
parameters: components:
parameters:
IdInQuery: IdInQuery:
in: "query" in: "query"
name: "id" name: "id"
allOf: schema:
- $ref: "#/definitions/Key" $ref: "#/components/schemas/Key"
IdsInBody:
in: "body"
name: "body"
type: array
items:
$ref: "#/definitions/Key"
NewValuesInBody:
in: "body"
name: "body"
type: array
allOf:
- $ref: "#/definitions/NewValues"
NewValuesWithIdsInBody:
in: "body"
name: "body"
type: array
items:
allOf:
- $ref: "#/definitions/Pair"
- properties:
first:
$ref: "#/definitions/Key"
second:
$ref: "#/definitions/NewValue"
PaginationInQueryPage: PaginationInQueryPage:
in: "query" in: "query"
type: integer
name: "ppage" name: "ppage"
description: "Page of pagination" description: "Page of pagination"
schema:
type: integer
required: false required: false
PaginationInQuerySize: PaginationInQuerySize:
in: "query" in: "query"
type: integer
name: "psize" name: "psize"
description: "Size of each page in pagination" description: "Size of each page in pagination"
schema:
type: integer
required: false required: false
schemas:
definitions:
Key: Key:
type: integer type: integer
description: "REWRITE THIS TYPE AS KEY IN SWAGGER FILE" description: "REWRITE THIS TYPE AS KEY IN SWAGGER FILE"
Value: Value:
type: integer type: integer
description: "REWRITE THIS TYPE AS VALUE IN SWAGGER FILE" description: "REWRITE THIS TYPE AS VALUE IN SWAGGER FILE"
Values:
type: array
items:
$ref: "#/definitions/Value"
NewValue: NewValue:
type: integer type: integer
description: "REWRITE THIS TYPE AS NEW VALUE IN SWAGGER FILE" description: "REWRITE THIS TYPE AS NEW VALUE IN SWAGGER FILE"
@@ -75,10 +49,6 @@ definitions:
properties: properties:
first: first:
second: second:
NewValues:
type: array
items:
$ref: "#/definitions/NewValue"
PaginationResult: PaginationResult:
type: object type: object
properties: properties:
@@ -103,30 +73,34 @@ paths:
tags: tags:
- "Read" - "Read"
parameters: parameters:
- $ref: "#/parameters/PaginationInQueryPage" - $ref: "#/components/parameters/PaginationInQueryPage"
- $ref: "#/parameters/PaginationInQuerySize" - $ref: "#/components/parameters/PaginationInQuerySize"
responses: responses:
"200": "200":
description: "Pagination with elements" description: "Pagination with elements"
content:
application/json:
schema: schema:
allOf: allOf:
- $ref: "#/definitions/PaginationResult" - $ref: "#/components/schemas/PaginationResult"
- properties: - properties:
results: results:
items: items:
$ref: "#/definitions/Value" $ref: "#/components/schemas/Value"
/getById: /getById:
get: get:
tags: tags:
- "Read" - "Read"
parameters: parameters:
- $ref: "#/parameters/IdInQuery" - $ref: "#/components/parameters/IdInQuery"
required: true required: true
responses: responses:
"200": "200":
description: "Result object" description: "Result object"
content:
application/json:
schema: schema:
$ref: "#/definitions/Value" $ref: "#/components/schemas/Value"
"204": "204":
description: "No value by id" description: "No value by id"
/contains: /contains:
@@ -134,11 +108,13 @@ paths:
tags: tags:
- "Read" - "Read"
parameters: parameters:
- $ref: "#/parameters/IdInQuery" - $ref: "#/components/parameters/IdInQuery"
required: true required: true
responses: responses:
"200": "200":
description: "Object with id availability in repo" description: "Object with id availability in repo"
content:
application/json:
schema: schema:
type: boolean type: boolean
/count: /count:
@@ -148,6 +124,8 @@ paths:
responses: responses:
"200": "200":
description: "Amount of objects in repo" description: "Amount of objects in repo"
content:
application/json:
schema: schema:
type: integer type: integer
@@ -156,32 +134,65 @@ paths:
post: post:
tags: tags:
- "Write" - "Write"
parameters: requestBody:
- $ref: "#/parameters/NewValuesInBody" content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/NewValue"
responses: responses:
"200": "200":
description: "Objects has been created and saved" description: "Objects has been created and saved"
content:
application/json:
schema: schema:
$ref: "#/definitions/Values" type: array
items:
$ref: "#/components/schemas/Value"
/update: /update:
post: post:
tags: tags:
- "Write" - "Write"
parameters: requestBody:
- $ref: "#/parameters/NewValuesWithIdsInBody" content:
application/json:
schema:
type: array
items:
allOf:
- $ref: "#/components/schemas/Pair"
- properties:
first:
$ref: "#/components/schemas/Key"
second:
$ref: "#/components/schemas/NewValue"
responses: responses:
"200": "200":
description: "Objects has been updated" description: "Objects has been updated"
content:
application/json:
schema: schema:
$ref: "#/definitions/Values" type: array
items:
$ref: "#/components/schemas/Value"
/deleteById: /deleteById:
post: post:
tags: tags:
- "Write" - "Write"
parameters: requestBody:
- $ref: "#/parameters/IdsInBody" content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Key"
responses: responses:
"200": "200":
description: "Objects has been updated" description: "Objects has been updated"
content:
application/json:
schema: schema:
$ref: "#/definitions/Values" type: array
items:
$ref: "#/components/schemas/Value"