Compare commits

...

16 Commits

11 changed files with 323 additions and 36 deletions

View File

@@ -1,5 +1,41 @@
# Changelog # Changelog
## 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 ## 0.9.6
* `Repos`: * `Repos`:

View File

@@ -153,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
inline fun <T> Iterable<T>.calculateStrictDiff( inline fun <T> Iterable<T>.calculateStrictDiff(
other: Iterable<T> other: Iterable<T>
) = calculateDiff(other, strictComparison = true) ) = 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)
}
}

View File

@@ -54,7 +54,7 @@ class DiffUtilsTests {
val oldList = (0 until 10).map { it.toString() } val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex() val withIndex = oldList.withIndex()
for (step in 0 until oldList.size) { for (step in oldList.indices) {
for ((i, v) in withIndex) { for ((i, v) in withIndex) {
val mutable = oldList.toMutableList() val mutable = oldList.toMutableList()
val changes = ( 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
)
}
}
}
} }

View File

@@ -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)
}

View File

@@ -10,11 +10,11 @@ org.gradle.jvmargs=-Xmx2g
kotlin_version=1.6.10 kotlin_version=1.6.10
kotlin_coroutines_version=1.6.0 kotlin_coroutines_version=1.6.0
kotlin_serialisation_core_version=1.3.2 kotlin_serialisation_core_version=1.3.2
kotlin_exposed_version=0.37.2 kotlin_exposed_version=0.37.3
ktor_version=1.6.7 ktor_version=1.6.7
klockVersion=2.4.13 klockVersion=2.6.1
github_release_plugin_version=2.2.12 github_release_plugin_version=2.2.12
@@ -24,7 +24,7 @@ uuidVersion=0.4.0
core_ktx_version=1.7.0 core_ktx_version=1.7.0
androidx_recycler_version=1.2.1 androidx_recycler_version=1.2.1
appcompat_version=1.4.0 appcompat_version=1.4.1
android_minSdkVersion=19 android_minSdkVersion=19
android_compileSdkVersion=32 android_compileSdkVersion=32
@@ -45,5 +45,5 @@ dokka_version=1.6.10
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.9.6 version=0.9.10
android_code_version=96 android_code_version=100

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.ws 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.Frame
import io.ktor.http.cio.websocket.readBytes import io.ktor.http.cio.websocket.readBytes
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy
inline fun <T> HttpClient.createStandardWebsocketFlow( inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String, url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true }, crossinline checkReconnection: (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
crossinline conversation: suspend (StandardKtorSerialInputData) -> T crossinline conversation: suspend (StandardKtorSerialInputData) -> T
): Flow<T> { ): Flow<T> {
val correctedUrl = url.asCorrectWebSocketUrl val correctedUrl = url.asCorrectWebSocketUrl
@@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
do { do {
val reconnect = try { val reconnect = try {
safely { safely {
ws(correctedUrl) { ws(correctedUrl, requestBuilder) {
for (received in incoming) { for (received in incoming) {
when (received) { when (received) {
is Frame.Binary -> producerScope.send(conversation(received.readBytes())) is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
@@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String, url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true }, crossinline checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>, deserializer: DeserializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow( ) = createStandardWebsocketFlow(
url, url,
checkReconnection checkReconnection,
requestBuilder
) { ) {
serialFormat.decodeDefault(deserializer, it) serialFormat.decodeDefault(deserializer, it)
} }

View File

@@ -85,9 +85,16 @@ class UnifiedRequester(
fun <T> createStandardWebsocketFlow( fun <T> createStandardWebsocketFlow(
url: String, url: String,
checkReconnection: (Throwable?) -> Boolean = { true }, checkReconnection: (Throwable?) -> Boolean,
deserializer: DeserializationStrategy<T> deserializer: DeserializationStrategy<T>,
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat) 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() val defaultRequester = UnifiedRequester()

View File

@@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server
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.*
import io.ktor.application.featureOrNull
import io.ktor.application.install
import io.ktor.http.URLProtocol
import io.ktor.http.cio.websocket.* import io.ktor.http.cio.websocket.*
import io.ktor.routing.Route 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.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
if (incoming.tryReceive() != null) {
close()
throw CorrectCloseException
}
}
fun <T> Route.includeWebsocketHandling( fun <T> Route.includeWebsocketHandling(
suburl: String, suburl: String,
flow: Flow<T>, 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 { safely {
flow.collect { flow.collect {
send(converter(it)) converter(it) ?.let { data ->
send(data)
}
} }
} }
} }
@@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling(
suburl: String, suburl: String,
flow: Flow<T>, flow: Flow<T>,
serializer: SerializationStrategy<T>, serializer: SerializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
protocol: URLProtocol = URLProtocol.WS,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling( ) = includeWebsocketHandling(
suburl, suburl,
flow flow,
) { protocol,
serialFormat.encodeDefault(serializer, it) converter = if (filter == null) {
} {
serialFormat.encodeDefault(serializer, it)
}
} else {
{
if (filter(it)) {
serialFormat.encodeDefault(serializer, it)
} else {
null
}
}
}
)

View File

@@ -5,8 +5,7 @@ import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.ContentType import io.ktor.http.*
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart import io.ktor.http.content.forEachPart
import io.ktor.request.receive import io.ktor.request.receive
@@ -18,6 +17,7 @@ import io.ktor.util.asStream
import io.ktor.util.cio.writeChannel import io.ktor.util.cio.writeChannel
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.* import io.ktor.utils.io.core.*
import io.ktor.websocket.WebSocketServerSession
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.* import kotlinx.serialization.*
import java.io.File import java.io.File
@@ -30,8 +30,10 @@ class UnifiedRouter(
fun <T> Route.includeWebsocketHandling( fun <T> Route.includeWebsocketHandling(
suburl: String, suburl: String,
flow: Flow<T>, flow: Flow<T>,
serializer: SerializationStrategy<T> serializer: SerializationStrategy<T>,
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat) protocol: URLProtocol = URLProtocol.WS,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
answerSerializer: SerializationStrategy<T>, answerSerializer: SerializationStrategy<T>,

View File

@@ -35,10 +35,15 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) { if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
return@mapNotNull null return@mapNotNull null
} }
insertIgnore { val insertResult = insert {
it[keyColumn] = k it[keyColumn] = k
it[valueColumn] = v it[valueColumn] = v
}.getOrNull(keyColumn) ?.let { k to v } }
if (insertResult.insertedCount > 0) {
k to v
} else {
null
}
} ?: emptyList() } ?: emptyList()
} }
}.forEach { _onNewValue.emit(it) } }.forEach { _onNewValue.emit(it) }