diff --git a/core/api/core.api b/core/api/core.api index a038d4920bb..fb061317750 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -2184,6 +2184,7 @@ public abstract class dev/kord/core/builder/kord/BaseKordBuilder { public final fun cache (Lkotlin/jvm/functions/Function2;)V public final fun gateways (Lkotlin/jvm/functions/Function2;)V public final fun getApplicationId ()Ldev/kord/common/entity/Snowflake; + public final fun getCompression ()Ldev/kord/gateway/Compression; public final fun getDefaultDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun getDefaultStrategy ()Ldev/kord/core/supplier/EntitySupplyStrategy; public final fun getEventFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; @@ -2193,6 +2194,7 @@ public abstract class dev/kord/core/builder/kord/BaseKordBuilder { public final fun getToken ()Ljava/lang/String; public final fun requestHandler (Lkotlin/jvm/functions/Function1;)V public final fun setApplicationId (Ldev/kord/common/entity/Snowflake;)V + public final fun setCompression (Ldev/kord/gateway/Compression;)V public final fun setDefaultDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V public final fun setDefaultStrategy (Ldev/kord/core/supplier/EntitySupplyStrategy;)V public final fun setEventFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)V diff --git a/core/api/core.klib.api b/core/api/core.klib.api index 23bc8491c80..7d5ace07109 100644 --- a/core/api/core.klib.api +++ b/core/api/core.klib.api @@ -1674,6 +1674,9 @@ abstract class dev.kord.core.builder.kord/BaseKordBuilder { // dev.kord.core.bui final var applicationId // dev.kord.core.builder.kord/BaseKordBuilder.applicationId|{}applicationId[0] final fun (): dev.kord.common.entity/Snowflake? // dev.kord.core.builder.kord/BaseKordBuilder.applicationId.|(){}[0] final fun (dev.kord.common.entity/Snowflake?) // dev.kord.core.builder.kord/BaseKordBuilder.applicationId.|(dev.kord.common.entity.Snowflake?){}[0] + final var compression // dev.kord.core.builder.kord/BaseKordBuilder.compression|{}compression[0] + final fun (): dev.kord.gateway/Compression // dev.kord.core.builder.kord/BaseKordBuilder.compression.|(){}[0] + final fun (dev.kord.gateway/Compression) // dev.kord.core.builder.kord/BaseKordBuilder.compression.|(dev.kord.gateway.Compression){}[0] final var defaultDispatcher // dev.kord.core.builder.kord/BaseKordBuilder.defaultDispatcher|{}defaultDispatcher[0] final fun (): kotlinx.coroutines/CoroutineDispatcher // dev.kord.core.builder.kord/BaseKordBuilder.defaultDispatcher.|(){}[0] final fun (kotlinx.coroutines/CoroutineDispatcher) // dev.kord.core.builder.kord/BaseKordBuilder.defaultDispatcher.|(kotlinx.coroutines.CoroutineDispatcher){}[0] diff --git a/core/src/commonMain/kotlin/builder/kord/KordBuilder.kt b/core/src/commonMain/kotlin/builder/kord/KordBuilder.kt index 04272a75633..a1909c90164 100644 --- a/core/src/commonMain/kotlin/builder/kord/KordBuilder.kt +++ b/core/src/commonMain/kotlin/builder/kord/KordBuilder.kt @@ -15,6 +15,7 @@ import dev.kord.core.gateway.DefaultMasterGateway import dev.kord.core.gateway.handler.DefaultGatewayEventInterceptor import dev.kord.core.gateway.handler.GatewayEventInterceptor import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.gateway.Compression import dev.kord.gateway.DefaultGateway import dev.kord.gateway.Gateway import dev.kord.gateway.builder.Shards @@ -49,6 +50,7 @@ public abstract class BaseKordBuilder internal constructor(public val token: Str DefaultGateway { client = resources.httpClient identifyRateLimiter = rateLimiter + compression = this@BaseKordBuilder.compression } } } @@ -57,6 +59,11 @@ public abstract class BaseKordBuilder internal constructor(public val token: Str { KtorRequestHandler(it.httpClient, ExclusionRequestRateLimiter(), token = token) } private var cacheBuilder: KordCacheBuilder.(resources: ClientResources) -> Unit = {} + /** + * The [compression mode][Compression] used. + */ + public var compression: Compression = Compression.ZLib + /** * Enables stack trace recovery on the currently defined [RequestHandler]. * diff --git a/gateway/api/gateway.api b/gateway/api/gateway.api index b73047ebda7..5b7c35693e0 100644 --- a/gateway/api/gateway.api +++ b/gateway/api/gateway.api @@ -219,6 +219,43 @@ public final class dev/kord/gateway/Command$SerializationStrategy : kotlinx/seri public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } +public abstract interface class dev/kord/gateway/Compression { + public abstract fun getName ()Ljava/lang/String; + public abstract fun newDecompressor ()Ldev/kord/gateway/Decompressor; +} + +public final class dev/kord/gateway/Compression$None : dev/kord/gateway/Compression { + public static final field INSTANCE Ldev/kord/gateway/Compression$None; + public fun equals (Ljava/lang/Object;)Z + public fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun newDecompressor ()Ldev/kord/gateway/Decompressor; + public fun toString ()Ljava/lang/String; +} + +public final class dev/kord/gateway/Compression$ZLib : dev/kord/gateway/Compression { + public static final field INSTANCE Ldev/kord/gateway/Compression$ZLib; + public fun equals (Ljava/lang/Object;)Z + public fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun newDecompressor ()Ldev/kord/gateway/Decompressor; + public fun toString ()Ljava/lang/String; +} + +public final class dev/kord/gateway/Compression$Zstd : dev/kord/gateway/Compression { + public static final field INSTANCE Ldev/kord/gateway/Compression$Zstd; + public fun equals (Ljava/lang/Object;)Z + public fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun newDecompressor ()Ldev/kord/gateway/Decompressor; + public fun toString ()Ljava/lang/String; +} + +public final class dev/kord/gateway/Decompressor$Noop : dev/kord/gateway/Decompressor { + public fun close ()V + public fun decompress (Lio/ktor/websocket/Frame;)Ljava/lang/String; +} + public final class dev/kord/gateway/DefaultGateway : dev/kord/gateway/Gateway { public static final field Companion Ldev/kord/gateway/DefaultGateway$Companion; public fun (Ldev/kord/gateway/DefaultGatewayData;)V @@ -238,6 +275,7 @@ public final class dev/kord/gateway/DefaultGatewayBuilder { public fun ()V public final fun build ()Ldev/kord/gateway/DefaultGateway; public final fun getClient ()Lio/ktor/client/HttpClient; + public final fun getCompression ()Ldev/kord/gateway/Compression; public final fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun getEventFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public final fun getIdentifyRateLimiter ()Ldev/kord/gateway/ratelimit/IdentifyRateLimiter; @@ -245,6 +283,7 @@ public final class dev/kord/gateway/DefaultGatewayBuilder { public final fun getSendRateLimiter ()Ldev/kord/common/ratelimit/RateLimiter; public final fun getUrl ()Ljava/lang/String; public final fun setClient (Lio/ktor/client/HttpClient;)V + public final fun setCompression (Ldev/kord/gateway/Compression;)V public final fun setDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V public final fun setEventFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)V public final fun setIdentifyRateLimiter (Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;)V @@ -254,7 +293,7 @@ public final class dev/kord/gateway/DefaultGatewayBuilder { } public final class dev/kord/gateway/DefaultGatewayData { - public fun (Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;)V + public fun (Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;Ldev/kord/gateway/Compression;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lio/ktor/client/HttpClient; public final fun component3 ()Ldev/kord/gateway/retry/Retry; @@ -262,10 +301,12 @@ public final class dev/kord/gateway/DefaultGatewayData { public final fun component5 ()Ldev/kord/gateway/ratelimit/IdentifyRateLimiter; public final fun component6 ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun component7 ()Lkotlinx/coroutines/flow/MutableSharedFlow; - public final fun copy (Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;)Ldev/kord/gateway/DefaultGatewayData; - public static synthetic fun copy$default (Ldev/kord/gateway/DefaultGatewayData;Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;ILjava/lang/Object;)Ldev/kord/gateway/DefaultGatewayData; + public final fun component8 ()Ldev/kord/gateway/Compression; + public final fun copy (Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;Ldev/kord/gateway/Compression;)Ldev/kord/gateway/DefaultGatewayData; + public static synthetic fun copy$default (Ldev/kord/gateway/DefaultGatewayData;Ljava/lang/String;Lio/ktor/client/HttpClient;Ldev/kord/gateway/retry/Retry;Ldev/kord/common/ratelimit/RateLimiter;Ldev/kord/gateway/ratelimit/IdentifyRateLimiter;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/flow/MutableSharedFlow;Ldev/kord/gateway/Compression;ILjava/lang/Object;)Ldev/kord/gateway/DefaultGatewayData; public fun equals (Ljava/lang/Object;)Z public final fun getClient ()Lio/ktor/client/HttpClient; + public final fun getCompression ()Ldev/kord/gateway/Compression; public final fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun getEventFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public final fun getIdentifyRateLimiter ()Ldev/kord/gateway/ratelimit/IdentifyRateLimiter; diff --git a/gateway/api/gateway.klib.api b/gateway/api/gateway.klib.api index b2e60f175ae..47c5a236a47 100644 --- a/gateway/api/gateway.klib.api +++ b/gateway/api/gateway.klib.api @@ -94,6 +94,43 @@ abstract interface dev.kord.gateway/Gateway : kotlinx.coroutines/CoroutineScope } } +sealed interface dev.kord.gateway/Compression { // dev.kord.gateway/Compression|null[0] + abstract val name // dev.kord.gateway/Compression.name|{}name[0] + abstract fun (): kotlin/String? // dev.kord.gateway/Compression.name.|(){}[0] + + abstract fun newDecompressor(): dev.kord.gateway/Decompressor // dev.kord.gateway/Compression.newDecompressor|newDecompressor(){}[0] + + final object None : dev.kord.gateway/Compression { // dev.kord.gateway/Compression.None|null[0] + final val name // dev.kord.gateway/Compression.None.name|{}name[0] + final fun (): kotlin/String? // dev.kord.gateway/Compression.None.name.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // dev.kord.gateway/Compression.None.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.kord.gateway/Compression.None.hashCode|hashCode(){}[0] + final fun newDecompressor(): dev.kord.gateway/Decompressor // dev.kord.gateway/Compression.None.newDecompressor|newDecompressor(){}[0] + final fun toString(): kotlin/String // dev.kord.gateway/Compression.None.toString|toString(){}[0] + } + + final object ZLib : dev.kord.gateway/Compression { // dev.kord.gateway/Compression.ZLib|null[0] + final val name // dev.kord.gateway/Compression.ZLib.name|{}name[0] + final fun (): kotlin/String // dev.kord.gateway/Compression.ZLib.name.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // dev.kord.gateway/Compression.ZLib.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.kord.gateway/Compression.ZLib.hashCode|hashCode(){}[0] + final fun newDecompressor(): dev.kord.gateway/Decompressor // dev.kord.gateway/Compression.ZLib.newDecompressor|newDecompressor(){}[0] + final fun toString(): kotlin/String // dev.kord.gateway/Compression.ZLib.toString|toString(){}[0] + } + + final object Zstd : dev.kord.gateway/Compression { // dev.kord.gateway/Compression.Zstd|null[0] + final val name // dev.kord.gateway/Compression.Zstd.name|{}name[0] + final fun (): kotlin/String // dev.kord.gateway/Compression.Zstd.name.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // dev.kord.gateway/Compression.Zstd.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.kord.gateway/Compression.Zstd.hashCode|hashCode(){}[0] + final fun newDecompressor(): dev.kord.gateway/Decompressor // dev.kord.gateway/Compression.Zstd.newDecompressor|newDecompressor(){}[0] + final fun toString(): kotlin/String // dev.kord.gateway/Compression.Zstd.toString|toString(){}[0] + } +} + final class dev.kord.gateway.builder/LoginBuilder { // dev.kord.gateway.builder/LoginBuilder|null[0] constructor () // dev.kord.gateway.builder/LoginBuilder.|(){}[0] @@ -405,6 +442,9 @@ final class dev.kord.gateway/DefaultGatewayBuilder { // dev.kord.gateway/Default final var client // dev.kord.gateway/DefaultGatewayBuilder.client|{}client[0] final fun (): io.ktor.client/HttpClient? // dev.kord.gateway/DefaultGatewayBuilder.client.|(){}[0] final fun (io.ktor.client/HttpClient?) // dev.kord.gateway/DefaultGatewayBuilder.client.|(io.ktor.client.HttpClient?){}[0] + final var compression // dev.kord.gateway/DefaultGatewayBuilder.compression|{}compression[0] + final fun (): dev.kord.gateway/Compression // dev.kord.gateway/DefaultGatewayBuilder.compression.|(){}[0] + final fun (dev.kord.gateway/Compression) // dev.kord.gateway/DefaultGatewayBuilder.compression.|(dev.kord.gateway.Compression){}[0] final var dispatcher // dev.kord.gateway/DefaultGatewayBuilder.dispatcher|{}dispatcher[0] final fun (): kotlinx.coroutines/CoroutineDispatcher // dev.kord.gateway/DefaultGatewayBuilder.dispatcher.|(){}[0] final fun (kotlinx.coroutines/CoroutineDispatcher) // dev.kord.gateway/DefaultGatewayBuilder.dispatcher.|(kotlinx.coroutines.CoroutineDispatcher){}[0] @@ -428,10 +468,12 @@ final class dev.kord.gateway/DefaultGatewayBuilder { // dev.kord.gateway/Default } final class dev.kord.gateway/DefaultGatewayData { // dev.kord.gateway/DefaultGatewayData|null[0] - constructor (kotlin/String, io.ktor.client/HttpClient, dev.kord.gateway.retry/Retry, dev.kord.common.ratelimit/RateLimiter, dev.kord.gateway.ratelimit/IdentifyRateLimiter, kotlinx.coroutines/CoroutineDispatcher, kotlinx.coroutines.flow/MutableSharedFlow) // dev.kord.gateway/DefaultGatewayData.|(kotlin.String;io.ktor.client.HttpClient;dev.kord.gateway.retry.Retry;dev.kord.common.ratelimit.RateLimiter;dev.kord.gateway.ratelimit.IdentifyRateLimiter;kotlinx.coroutines.CoroutineDispatcher;kotlinx.coroutines.flow.MutableSharedFlow){}[0] + constructor (kotlin/String, io.ktor.client/HttpClient, dev.kord.gateway.retry/Retry, dev.kord.common.ratelimit/RateLimiter, dev.kord.gateway.ratelimit/IdentifyRateLimiter, kotlinx.coroutines/CoroutineDispatcher, kotlinx.coroutines.flow/MutableSharedFlow, dev.kord.gateway/Compression) // dev.kord.gateway/DefaultGatewayData.|(kotlin.String;io.ktor.client.HttpClient;dev.kord.gateway.retry.Retry;dev.kord.common.ratelimit.RateLimiter;dev.kord.gateway.ratelimit.IdentifyRateLimiter;kotlinx.coroutines.CoroutineDispatcher;kotlinx.coroutines.flow.MutableSharedFlow;dev.kord.gateway.Compression){}[0] final val client // dev.kord.gateway/DefaultGatewayData.client|{}client[0] final fun (): io.ktor.client/HttpClient // dev.kord.gateway/DefaultGatewayData.client.|(){}[0] + final val compression // dev.kord.gateway/DefaultGatewayData.compression|{}compression[0] + final fun (): dev.kord.gateway/Compression // dev.kord.gateway/DefaultGatewayData.compression.|(){}[0] final val dispatcher // dev.kord.gateway/DefaultGatewayData.dispatcher|{}dispatcher[0] final fun (): kotlinx.coroutines/CoroutineDispatcher // dev.kord.gateway/DefaultGatewayData.dispatcher.|(){}[0] final val eventFlow // dev.kord.gateway/DefaultGatewayData.eventFlow|{}eventFlow[0] @@ -452,7 +494,8 @@ final class dev.kord.gateway/DefaultGatewayData { // dev.kord.gateway/DefaultGat final fun component5(): dev.kord.gateway.ratelimit/IdentifyRateLimiter // dev.kord.gateway/DefaultGatewayData.component5|component5(){}[0] final fun component6(): kotlinx.coroutines/CoroutineDispatcher // dev.kord.gateway/DefaultGatewayData.component6|component6(){}[0] final fun component7(): kotlinx.coroutines.flow/MutableSharedFlow // dev.kord.gateway/DefaultGatewayData.component7|component7(){}[0] - final fun copy(kotlin/String = ..., io.ktor.client/HttpClient = ..., dev.kord.gateway.retry/Retry = ..., dev.kord.common.ratelimit/RateLimiter = ..., dev.kord.gateway.ratelimit/IdentifyRateLimiter = ..., kotlinx.coroutines/CoroutineDispatcher = ..., kotlinx.coroutines.flow/MutableSharedFlow = ...): dev.kord.gateway/DefaultGatewayData // dev.kord.gateway/DefaultGatewayData.copy|copy(kotlin.String;io.ktor.client.HttpClient;dev.kord.gateway.retry.Retry;dev.kord.common.ratelimit.RateLimiter;dev.kord.gateway.ratelimit.IdentifyRateLimiter;kotlinx.coroutines.CoroutineDispatcher;kotlinx.coroutines.flow.MutableSharedFlow){}[0] + final fun component8(): dev.kord.gateway/Compression // dev.kord.gateway/DefaultGatewayData.component8|component8(){}[0] + final fun copy(kotlin/String = ..., io.ktor.client/HttpClient = ..., dev.kord.gateway.retry/Retry = ..., dev.kord.common.ratelimit/RateLimiter = ..., dev.kord.gateway.ratelimit/IdentifyRateLimiter = ..., kotlinx.coroutines/CoroutineDispatcher = ..., kotlinx.coroutines.flow/MutableSharedFlow = ..., dev.kord.gateway/Compression = ...): dev.kord.gateway/DefaultGatewayData // dev.kord.gateway/DefaultGatewayData.copy|copy(kotlin.String;io.ktor.client.HttpClient;dev.kord.gateway.retry.Retry;dev.kord.common.ratelimit.RateLimiter;dev.kord.gateway.ratelimit.IdentifyRateLimiter;kotlinx.coroutines.CoroutineDispatcher;kotlinx.coroutines.flow.MutableSharedFlow;dev.kord.gateway.Compression){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // dev.kord.gateway/DefaultGatewayData.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // dev.kord.gateway/DefaultGatewayData.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // dev.kord.gateway/DefaultGatewayData.toString|toString(){}[0] diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index 2b646a41100..1452fe924ea 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -19,12 +19,14 @@ kotlin { jvmMain { dependencies { implementation(libs.slf4j.api) + implementation(libs.zstd.jni) } } jsMain { dependencies { implementation(libs.kotlin.node) implementation(npm("fast-zlib", libs.versions.fastZlib.get())) + implementation(npm("fzstd", libs.versions.fzstd.get())) // workaround for https://youtrack.jetbrains.com/issue/KT-43500 / // https://youtrack.jetbrains.com/issue/KT-64109#focus=Comments-27-10064206.0-0 / diff --git a/gateway/src/commonMain/kotlin/Compression.kt b/gateway/src/commonMain/kotlin/Compression.kt new file mode 100644 index 00000000000..598637c4c6f --- /dev/null +++ b/gateway/src/commonMain/kotlin/Compression.kt @@ -0,0 +1,54 @@ +@file:Suppress("FunctionName") + +package dev.kord.gateway + +import dev.kord.common.annotation.KordInternal +import io.ktor.websocket.* + +/** @suppress */ +@KordInternal // Only public for interface, binary API might change at any time +public interface Decompressor : AutoCloseable { + public fun Frame.decompress(): String + + public companion object Noop : Decompressor { + override fun Frame.decompress(): String = data.decodeToString() + override fun close() {} + } +} + +internal expect fun ZLibDecompressor(): Decompressor +internal expect fun ZstdDecompressor(): Decompressor + +/** + * Different compression modes for the Discord gateway. + * + * @property name the name used by the Discord API + */ +public sealed interface Compression { + public val name: String? + public fun newDecompressor(): Decompressor + + /** + * Implementation using no compression. + */ + public data object None : Compression { + override val name: String? = null + override fun newDecompressor(): Decompressor = Decompressor.Noop + } + + /** + * Implementation using [zlib](https://zlib.net/). + */ + public data object ZLib : Compression { + override val name: String = "zlib-stream" + override fun newDecompressor(): Decompressor = ZLibDecompressor() + } + + /** + * Implementation using [Zstandard](https://facebook.github.io/zstd/) + */ + public data object Zstd : Compression { + override val name: String = "zstd-stream" + override fun newDecompressor(): Decompressor = ZstdDecompressor() + } +} diff --git a/gateway/src/commonMain/kotlin/DefaultGateway.kt b/gateway/src/commonMain/kotlin/DefaultGateway.kt index 8a5f673ffc0..5847a68a4f9 100644 --- a/gateway/src/commonMain/kotlin/DefaultGateway.kt +++ b/gateway/src/commonMain/kotlin/DefaultGateway.kt @@ -39,12 +39,13 @@ private sealed class State(val retry: Boolean) { } /** - * @param url The url to connect to. - * @param client The [HttpClient] from which a WebSocket will be created, requires the [WebSockets] plugin to be + * @property url The url to connect to. + * @property client The [HttpClient] from which a WebSocket will be created, requires the [WebSockets] plugin to be * installed. - * @param reconnectRetry A [Retry] used for reconnection attempts. - * @param sendRateLimiter A [RateLimiter] that follows the Discord API specifications for sending messages. - * @param identifyRateLimiter An [IdentifyRateLimiter] that follows the Discord API specifications for identifying. + * @property reconnectRetry A [Retry] used for reconnection attempts. + * @property sendRateLimiter A [RateLimiter] that follows the Discord API specifications for sending messages. + * @property identifyRateLimiter An [IdentifyRateLimiter] that follows the Discord API specifications for identifying. + * @property compression the [compression mode][Compression] used */ public data class DefaultGatewayData( val url: String, @@ -54,6 +55,7 @@ public data class DefaultGatewayData( val identifyRateLimiter: IdentifyRateLimiter, val dispatcher: CoroutineDispatcher, val eventFlow: MutableSharedFlow, + val compression: Compression, ) /** @@ -63,8 +65,6 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { override val coroutineContext: CoroutineContext = SupervisorJob() + data.dispatcher - private val compression: Boolean - private val _ping = MutableStateFlow(null) override val ping: StateFlow get() = _ping @@ -76,7 +76,7 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { private val handshakeHandler: HandshakeHandler - private lateinit var inflater: Inflater + private lateinit var decompressor: Decompressor private val jsonParser = Json { ignoreUnknownKeys = true @@ -86,9 +86,10 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { private val stateMutex = Mutex() init { - val initialUrl = Url(data.url) - compression = initialUrl.parameters.contains("compress", "zlib-stream") - + val initialUrl = URLBuilder(data.url).apply { + val compressionName = data.compression.name ?: return@apply + parameters.append("compress", compressionName) + }.build() val sequence = Sequence() SequenceHandler(events, sequence) handshakeHandler = HandshakeHandler(events, initialUrl, ::trySend, sequence, data.reconnectRetry) @@ -117,7 +118,11 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { * * > Every connection to the gateway should use its own unique zlib context. */ - inflater = Inflater() + try { + decompressor = data.compression.newDecompressor() + } catch (e: Throwable) { + e.printStackTrace() + } } catch (exception: Exception) { defaultGatewayLogger.error(exception) { "" } if (exception.isTimeout()) { @@ -179,10 +184,7 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { private suspend fun read(frame: Frame) { defaultGatewayLogger.trace { "Received raw frame: $frame" } - val json = when { - compression -> with(inflater) { frame.inflateData() } - else -> frame.data.decodeToString() - } + val json = with(decompressor) { frame.decompress() } try { defaultGatewayLogger.trace { "Gateway <<< $json" } @@ -195,7 +197,7 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { } private suspend fun handleClose() { - inflater.close() + decompressor.close() val reason = withTimeoutOrNull(1500) { socket.closeReason.await() @@ -211,6 +213,7 @@ public class DefaultGateway(private val data: DefaultGatewayData) : Gateway { state.update { State.Stopped } throw IllegalStateException("Gateway closed: ${reason.code} ${reason.message}") } + discordReason.resetSession -> { setStopped() } diff --git a/gateway/src/commonMain/kotlin/DefaultGatewayBuilder.kt b/gateway/src/commonMain/kotlin/DefaultGatewayBuilder.kt index 7dd8febc9cc..cf840cb4ddb 100644 --- a/gateway/src/commonMain/kotlin/DefaultGatewayBuilder.kt +++ b/gateway/src/commonMain/kotlin/DefaultGatewayBuilder.kt @@ -18,13 +18,14 @@ import kotlin.time.Duration.Companion.seconds public class DefaultGatewayBuilder { public var url: String = - "wss://gateway.discord.gg/?v=${KordConfiguration.GATEWAY_VERSION}&encoding=json&compress=zlib-stream" + "wss://gateway.discord.gg/?v=${KordConfiguration.GATEWAY_VERSION}&encoding=json" public var client: HttpClient? = null public var reconnectRetry: Retry? = null public var sendRateLimiter: RateLimiter? = null public var identifyRateLimiter: IdentifyRateLimiter? = null public var dispatcher: CoroutineDispatcher = Dispatchers.Default public var eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + public var compression: Compression = Compression.ZLib public fun build(): DefaultGateway { val client = client ?: HttpClient(httpEngine()) { @@ -44,7 +45,8 @@ public class DefaultGatewayBuilder { sendRateLimiter, identifyRateLimiter, dispatcher, - eventFlow + eventFlow, + compression ) return DefaultGateway(data) diff --git a/gateway/src/commonMain/kotlin/Inflater.kt b/gateway/src/commonMain/kotlin/Inflater.kt deleted file mode 100644 index d8078372c4c..00000000000 --- a/gateway/src/commonMain/kotlin/Inflater.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.kord.gateway - -import io.ktor.websocket.* - -internal interface Inflater : AutoCloseable { - fun Frame.inflateData(): String -} - -internal expect fun Inflater(): Inflater diff --git a/gateway/src/jsMain/kotlin/Inflater.kt b/gateway/src/jsMain/kotlin/ZLibDecompressor.kt similarity index 76% rename from gateway/src/jsMain/kotlin/Inflater.kt rename to gateway/src/jsMain/kotlin/ZLibDecompressor.kt index 418c269d094..28dd1a67017 100644 --- a/gateway/src/jsMain/kotlin/Inflater.kt +++ b/gateway/src/jsMain/kotlin/ZLibDecompressor.kt @@ -5,10 +5,10 @@ import io.ktor.websocket.* import node.buffer.Buffer import node.buffer.BufferEncoding -internal actual fun Inflater() = object : Inflater { +internal actual fun ZLibDecompressor() = object : Decompressor { private val inflate = Inflate() - override fun Frame.inflateData(): String { + override fun Frame.decompress(): String { val buffer = Buffer.from(data) return inflate.process(buffer).toString(BufferEncoding.utf8) diff --git a/gateway/src/jsMain/kotlin/ZstdDecompressor.kt b/gateway/src/jsMain/kotlin/ZstdDecompressor.kt new file mode 100644 index 00000000000..da40ce044c6 --- /dev/null +++ b/gateway/src/jsMain/kotlin/ZstdDecompressor.kt @@ -0,0 +1,21 @@ +package dev.kord.gateway + +import dev.kord.gateway.internal.Decompress +import io.ktor.websocket.* +import js.typedarrays.toUint8Array + +internal actual fun ZstdDecompressor() = object : Decompressor { + private val stream = Decompress() + + override fun Frame.decompress(): String { + var cache = ByteArray(0) + stream.onData = { data, _ -> + cache += data.toByteArray() + } + // This call is sync, so if it finishes, the cache will store all the chunks + stream.push(data.toUint8Array()) + return cache.decodeToString() + } + + override fun close() = Unit +} diff --git a/gateway/src/jsMain/kotlin/internal/JsZstd.kt b/gateway/src/jsMain/kotlin/internal/JsZstd.kt new file mode 100644 index 00000000000..48f6bde336d --- /dev/null +++ b/gateway/src/jsMain/kotlin/internal/JsZstd.kt @@ -0,0 +1,11 @@ +@file:JsModule("fzstd") + +package dev.kord.gateway.internal + +import js.typedarrays.Uint8Array + +internal external class Decompress { + @JsName("ondata") + var onData: (data: Uint8Array, final: Boolean) -> Unit + fun push(chunk: Uint8Array, final: Boolean = definedExternally) +} diff --git a/gateway/src/jvmMain/kotlin/Inflater.kt b/gateway/src/jvmMain/kotlin/ZLibDecompressor.kt similarity index 80% rename from gateway/src/jvmMain/kotlin/Inflater.kt rename to gateway/src/jvmMain/kotlin/ZLibDecompressor.kt index 348ba9ae389..14aba474830 100644 --- a/gateway/src/jvmMain/kotlin/Inflater.kt +++ b/gateway/src/jvmMain/kotlin/ZLibDecompressor.kt @@ -4,10 +4,10 @@ import io.ktor.websocket.* import java.io.ByteArrayOutputStream import java.util.zip.InflaterOutputStream -internal actual fun Inflater() = object : Inflater { +internal actual fun ZLibDecompressor() = object : Decompressor { private val delegate = java.util.zip.Inflater() - override fun Frame.inflateData(): String { + override fun Frame.decompress(): String { val outputStream = ByteArrayOutputStream() InflaterOutputStream(outputStream, delegate).use { it.write(data) diff --git a/gateway/src/jvmMain/kotlin/ZstdDecompressor.kt b/gateway/src/jvmMain/kotlin/ZstdDecompressor.kt new file mode 100644 index 00000000000..3c705bd0d39 --- /dev/null +++ b/gateway/src/jvmMain/kotlin/ZstdDecompressor.kt @@ -0,0 +1,28 @@ +package dev.kord.gateway + +import com.github.luben.zstd.ZstdInputStream +import io.ktor.websocket.* +import java.io.ByteArrayInputStream + +internal actual fun ZstdDecompressor() = object : Decompressor { + + private val input = UpdatableByteArrayInputStream() + private val zstdStream = ZstdInputStream(input).apply { continuous = true } + + override fun Frame.decompress(): String { + input.update(data) + return zstdStream.readBytes().decodeToString() + } + + override fun close() { + zstdStream.close() + } +} + +private class UpdatableByteArrayInputStream : ByteArrayInputStream(ByteArray(0)) { + fun update(newBuf: ByteArray) { + this.pos = 0 + this.buf = newBuf + this.count = newBuf.size + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1599989c8fa..2b48bc43818 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,9 @@ kotlin-node = "22.5.4-pre.818" # https://github.com/JetBrains/kotlin-wrappers bignum = "0.3.10" # https://github.com/ionspin/kotlin-multiplatform-bignum stately = "2.1.0" # https://github.com/touchlab/Stately fastZlib = "2.0.1" # https://github.com/timotejroiko/fast-zlib +fzstd = "0.1.1" # https://github.com/101arrowz/fzstd/ +zstd-jni = "1.5.6-7" # https://github.com/luben/zstd-jni +zstd-codec = "0.1.5" # https://www.npmjs.com/package/zstd-codec # code generation ksp = "2.0.21-1.0.25" # https://github.com/google/ksp @@ -63,6 +66,7 @@ kotlin-node = { module = "org.jetbrains.kotlin-wrappers:kotlin-node", version.re # JDK replacements bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } stately-collections = { module = "co.touchlab:stately-concurrent-collections", version.ref = "stately" } +zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd-jni" } # code generation ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 493392acc4e..b33d9525e4c 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -204,6 +204,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +fzstd@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/fzstd/-/fzstd-0.1.1.tgz#a3da29f2fff45070ca90073f866d97e0c56a4a52" + integrity sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" diff --git a/samples/src/commonMain/resources/simplelogger.properties b/samples/src/commonMain/resources/simplelogger.properties new file mode 100644 index 00000000000..b4378621551 --- /dev/null +++ b/samples/src/commonMain/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.defaultLogLevel=trace +org.slf4j.simpleLogger.showDateTime=true \ No newline at end of file