From 32caa99ee6750369c4cbf262aae3a4dadda131d5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 29 Jul 2021 00:31:31 +1200 Subject: [PATCH 01/34] Add kotlin realtime models --- .../main/java/io/appwrite/models/RealtimeCodes.kt.twig | 5 +++++ .../main/java/io/appwrite/models/RealtimeError.kt.twig | 6 ++++++ .../main/java/io/appwrite/models/RealtimeEvent.kt.twig | 7 +++++++ .../main/java/io/appwrite/models/RealtimeMessage.kt.twig | 9 +++++++++ .../java/io/appwrite/models/RealtimeSubscription.kt.twig | 5 +++++ 5 files changed, 32 insertions(+) create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig new file mode 100644 index 000000000..f4faa854d --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig @@ -0,0 +1,5 @@ +package {{ sdk.namespace | caseDot }}.models + +enum class RealtimeCodes(val value: Int) { + POLICY_VIOLATION(1008) +} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig new file mode 100644 index 000000000..593728d69 --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig @@ -0,0 +1,6 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeError( + val code: Int, + val message: String? +) \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig new file mode 100644 index 000000000..34ff6e00b --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig @@ -0,0 +1,7 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeEvent( + val project: String, + val permissions: List, + val data: String +) \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig new file mode 100644 index 000000000..edb3f956e --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig @@ -0,0 +1,9 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeMessage( + val code: Int?, + val event: String, + val channels: List, + val timestamp: Long, + val payload: Any +) \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig new file mode 100644 index 000000000..7434b1783 --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig @@ -0,0 +1,5 @@ +package {{ sdk.namespace | caseDot }}.models + +data class Subscription( + val close: () -> Unit +) \ No newline at end of file From 4b94c1f8b7eb0eee2f3c7a25b8e0eabfa0a115e9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 29 Jul 2021 00:31:53 +1200 Subject: [PATCH 02/34] Add kotlin foreach async function --- .../appwrite/extensions/CollectionExtensions.kt.twig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 templates/android/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt.twig diff --git a/templates/android/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt.twig b/templates/android/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt.twig new file mode 100644 index 000000000..1cae0bea0 --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt.twig @@ -0,0 +1,12 @@ +package {{ sdk.namespace | caseDot }}.extensions + +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext + +suspend fun Collection.forEachAsync( + callback: suspend (T) -> Unit +) = withContext(IO) { + map { async { callback.invoke(it) } }.awaitAll() +} \ No newline at end of file From 427e98149ebffd5ab166f063327b112838603b83 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 29 Jul 2021 00:32:37 +1200 Subject: [PATCH 03/34] Add realtime endpoint to kotlin client --- .../src/main/java/io/appwrite/Client.kt.twig | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 57e85e8c0..d0d34c2fd 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -36,6 +36,7 @@ import kotlin.coroutines.resume class Client @JvmOverloads constructor( context: Context, var endPoint: String = "https://appwrite.io/v1", + var endPointRealtime: String? = null, private var selfSigned: Boolean = false ) : CoroutineScope { @@ -151,7 +152,7 @@ class Client @JvmOverloads constructor( } /** - * Set endpoint + * Set endpoint and realtime endpoint. * * @param endpoint * @@ -159,6 +160,23 @@ class Client @JvmOverloads constructor( */ fun setEndpoint(endPoint: String): Client { this.endPoint = endPoint + + if (endPoint.startsWith("http")) { + this.endPointRealtime = endPoint.replace("http", "ws") + } + + return this + } + + /** + * Set realtime endpoint + * + * @param endpoint + * + * @return this + */ + fun setEndpointRealtime(endPoint: String): Client { + this.endPointRealtime = endPoint return this } From 60add68c34a0a533f01b0129e855e50a79b438bd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 29 Jul 2021 00:36:05 +1200 Subject: [PATCH 04/34] Add WIP kotlin realtime service --- .../java/io/appwrite/services/Realtime.kt | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 templates/android/library/src/main/java/io/appwrite/services/Realtime.kt diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt new file mode 100644 index 000000000..86e9e2f68 --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt @@ -0,0 +1,159 @@ +package {{ sdk.namespace | caseDot }}.services + +import android.util.Log +import com.google.gson.Gson +import com.google.gson.JsonParseException +import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.extensions.forEachAsync +import {{ sdk.namespace | caseDot }}.models.RealtimeCodes +import {{ sdk.namespace | caseDot }}.models.RealtimeError +import {{ sdk.namespace | caseDot }}.models.RealtimeMessage +import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import okhttp3.* +import okhttp3.internal.concurrent.TaskRunner +import okhttp3.internal.ws.RealWebSocket +import java.util.* +import kotlin.coroutines.CoroutineContext + +class Realtime(client: Client) : BaseService(client), CoroutineScope { + + private val job = Job() + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + private companion object { + private const val debounceDelayMillis = 20 + + private var socket: RealWebSocket? = null + private var lastMessage: RealtimeMessage? = null + private var channelCallbacks = mutableMapOf Unit>>() + + private var goingToConnect = false + private var subscribeReentered = false + } + + private fun createSocket() { + val queryParamBuilder = StringBuilder() + .append("project=") + .append(client.config["project"]) + + channelCallbacks.forEach { + queryParamBuilder + .append("&channels[]=") + .append(it.key) + } + + val request = Request.Builder() + .url("${client.endPointRealtime}/realtime?$queryParamBuilder") + .build() + + if (socket != null) { + socket?.close(1000, null) + } + + socket = RealWebSocket( + taskRunner = TaskRunner.INSTANCE, + originalRequest = request, + listener = {{ spec.title | caseUcFirst }}WebSocketListener(), + random = Random(), + pingIntervalMillis = client.http.pingIntervalMillis.toLong(), + extensions = null, + minimumDeflateSize = client.http.minWebSocketMessageToCompress + ) + + socket!!.connect(client.http) + } + + fun subscribe( + channels: Collection, + callback: (Any) -> Unit + ) : RealtimeSubscription { + if (goingToConnect) { + subscribeReentered = true + } + + channels.forEach { + channelCallbacks[it]?.add(callback) + } + + launch { + goingToConnect = true + delay(debounceDelayMillis.toLong()) + if (subscribeReentered) { + subscribeReentered = false + return@launch + } + + socket?.connect(client.http) + goingToConnect = false + subscribeReentered = false + } + + return RealtimeSubscription { + // TODO: Allow subscription closing + } + } + + fun subscribe(channel: String, callback: (Any) -> Unit) = + subscribe(listOf(channel), callback) + + private inner class {{ spec.title | caseUcFirst }}WebSocketListener : WebSocketListener() { + + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + Log.i(this@Realtime::class.java.simpleName, "WebSocket connected.") + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + Log.i(this@Realtime::class.java.simpleName, "WebSocket message received.") + + launch(IO) { + val message = try { + Gson().fromJson(text, RealtimeMessage::class.java) + } catch (ex: JsonParseException) { + val error = parseError(text) // TODO: How to propagate this to client? + throw ex + } ?: return@launch + + lastMessage = message + + message.channels.forEachAsync { channel -> + channelCallbacks[channel]?.forEachAsync { callback -> + callback.invoke(message.payload) + } + } + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + super.onClosing(webSocket, code, reason) + Log.i(this@Realtime::class.java.simpleName, "WebSocket closing with code $code because: $reason. Reconnecting in 1 second.") + if (lastMessage?.code == RealtimeCodes.POLICY_VIOLATION.value) { + return + } + launch { + delay(1000) + createSocket() + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + Log.e(this@Realtime::class.java.simpleName, "WebSocket failure.") + t.printStackTrace() + } + + private fun parseError(text: String) : RealtimeError { + return try { + Gson().fromJson(text, RealtimeError::class.java) + } catch (ex: JsonParseException) { + ex.printStackTrace() + throw ex + } + } + } +} \ No newline at end of file From c8a40745fedceee6841f660bd16c5ec22d225297 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:09:09 +1200 Subject: [PATCH 05/34] Add realtime files to android template --- src/SDK/Language/Android.php | 44 +++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 9db42edd8..ba48b2e77 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -134,12 +134,48 @@ public function getFiles() 'template' => '/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/extensions/CollectionExtensions.kt', + 'template' => '/android/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt.twig', + 'minify' => false, + ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/Error.kt', 'template' => '/android/library/src/main/java/io/appwrite/models/Error.kt.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeError.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeEvent.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeMessage.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeSubscription.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig', + 'minify' => false, + ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/WebAuthComponent.kt', @@ -160,10 +196,16 @@ public function getFiles() ], [ 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/services/BaseService.kt', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/services/Service.kt', 'template' => '/android/library/src/main/java/io/appwrite/services/Service.kt.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/services/Realtime.kt', + 'template' => '/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig', + 'minify' => false, + ], [ 'scope' => 'service', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', From f342383c344bb7f8cb12fc7ea23cbd90b34d179f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:09:40 +1200 Subject: [PATCH 06/34] Move JSON extensions to top level functions --- .../library/src/main/java/io/appwrite/Client.kt.twig | 2 +- .../io/appwrite/extensions/JsonExtensions.kt.twig | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index d0d34c2fd..a0f080286 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -5,7 +5,7 @@ import android.content.pm.PackageManager import com.google.gson.Gson import io.appwrite.appwrite.BuildConfig import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception -import {{ sdk.namespace | caseDot }}.extensions.JsonExtensions.fromJson +import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.models.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig index e05442224..a7a258a97 100644 --- a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig @@ -2,11 +2,8 @@ package {{ sdk.namespace | caseDot }}.extensions import com.google.gson.Gson -object JsonExtensions { +fun Any.toJson(): String = + Gson().toJson(this) - fun Any.toJson(): String = - Gson().toJson(this) - - fun String.fromJson(clazz: Class): T = - Gson().fromJson(this, clazz) -} \ No newline at end of file +fun String.fromJson(clazz: Class): T = + Gson().fromJson(this, clazz) From 4d6487591e982c7a939ea33f44668305e051bbe9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:10:12 +1200 Subject: [PATCH 07/34] Fix setting default realtime endpoint --- .../android/library/src/main/java/io/appwrite/Client.kt.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index a0f080286..a27b95f45 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -161,7 +161,7 @@ class Client @JvmOverloads constructor( fun setEndpoint(endPoint: String): Client { this.endPoint = endPoint - if (endPoint.startsWith("http")) { + if (this.endPointRealtime == null && endPoint.startsWith("http")) { this.endPointRealtime = endPoint.replace("http", "ws") } From 41e3e8066dc7019eae0e6d99cc4663cdc2c43f52 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:11:49 +1200 Subject: [PATCH 08/34] Make realtime subscription closeable for try-with-resources or .use extension constructs --- .../io/appwrite/models/RealtimeSubscription.kt.twig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig index 7434b1783..3eeeb8b91 100644 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig @@ -1,5 +1,9 @@ package {{ sdk.namespace | caseDot }}.models -data class Subscription( - val close: () -> Unit -) \ No newline at end of file +import java.io.Closeable + +data class RealtimeSubscription( + private val close: () -> Unit +) : Closeable { + override fun close() = close.invoke() +} \ No newline at end of file From d0d0cc7930bce4c90b5ab857ff549b301222e0bf Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:12:42 +1200 Subject: [PATCH 09/34] Make service name pattern consistent, make client protected in services --- .../library/src/main/java/io/appwrite/services/Service.kt.twig | 2 +- .../src/main/java/io/appwrite/services/ServiceTemplate.kt.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Service.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Service.kt.twig index 80ceeeffa..a6b5623fc 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Service.kt.twig @@ -2,4 +2,4 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Client -abstract class BaseService(private val client: Client) +abstract class Service(val client: Client) diff --git a/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig index 31989a7d3..d0fb41224 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig @@ -22,7 +22,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl {% endif %} import java.io.File -class {{ service.name | caseUcfirst }}(private val client: Client) : BaseService(client) { +class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { {% for method in service.methods %} /** From c6358f1cea45247922ad65e0b5881c30524e5e49 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:14:20 +1200 Subject: [PATCH 10/34] Add unsubscribe + subscription returns, fix message parsing, channel callback registration, socket close looping --- .../{Realtime.kt => Realtime.kt.twig} | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) rename templates/android/library/src/main/java/io/appwrite/services/{Realtime.kt => Realtime.kt.twig} (67%) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig similarity index 67% rename from templates/android/library/src/main/java/io/appwrite/services/Realtime.kt rename to templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 86e9e2f68..6bbe2915f 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -3,6 +3,7 @@ package {{ sdk.namespace | caseDot }}.services import android.util.Log import com.google.gson.Gson import com.google.gson.JsonParseException +import com.google.gson.JsonSyntaxException import {{ sdk.namespace | caseDot }}.Client import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.models.RealtimeCodes @@ -17,7 +18,7 @@ import okhttp3.internal.ws.RealWebSocket import java.util.* import kotlin.coroutines.CoroutineContext -class Realtime(client: Client) : BaseService(client), CoroutineScope { +class Realtime(client: Client) : Service(client), CoroutineScope { private val job = Job() @@ -28,8 +29,8 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { private const val debounceDelayMillis = 20 private var socket: RealWebSocket? = null - private var lastMessage: RealtimeMessage? = null private var channelCallbacks = mutableMapOf Unit>>() + private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() private var goingToConnect = false private var subscribeReentered = false @@ -37,13 +38,11 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { private fun createSocket() { val queryParamBuilder = StringBuilder() - .append("project=") - .append(client.config["project"]) + .append("project=${client.config["project"]}") - channelCallbacks.forEach { + channelCallbacks.keys.forEach { queryParamBuilder - .append("&channels[]=") - .append(it.key) + .append("&channels[]=$it") } val request = Request.Builder() @@ -51,24 +50,28 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { .build() if (socket != null) { - socket?.close(1000, null) + closeSocket() } socket = RealWebSocket( taskRunner = TaskRunner.INSTANCE, originalRequest = request, - listener = {{ spec.title | caseUcFirst }}WebSocketListener(), + listener = {{ spec.title | caseUcfirst }}WebSocketListener(), random = Random(), pingIntervalMillis = client.http.pingIntervalMillis.toLong(), extensions = null, minimumDeflateSize = client.http.minWebSocketMessageToCompress ) - socket!!.connect(client.http) + socket?.connect(client.http) + } + + private fun closeSocket() { + socket?.close(RealtimeCodes.POLICY_VIOLATION.value, null) } fun subscribe( - channels: Collection, + vararg channels: String, callback: (Any) -> Unit ) : RealtimeSubscription { if (goingToConnect) { @@ -76,6 +79,10 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { } channels.forEach { + if (!channelCallbacks.containsKey(it)) { + channelCallbacks[it] = mutableListOf(callback) + return@forEach + } channelCallbacks[it]?.add(callback) } @@ -87,20 +94,33 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { return@launch } - socket?.connect(client.http) + createSocket() goingToConnect = false subscribeReentered = false } return RealtimeSubscription { - // TODO: Allow subscription closing + unsubscribe(*channels) + } + } + + fun unsubscribe(vararg channels: String) { + channels.forEach { + channelCallbacks.remove(it) } } - fun subscribe(channel: String, callback: (Any) -> Unit) = - subscribe(listOf(channel), callback) + fun unsubscribe() { + channelCallbacks = mutableMapOf() + errorCallbacks = mutableSetOf() + closeSocket() + } + + fun doOnError(callback: (RealtimeError) -> Unit) { + errorCallbacks.add(callback) + } - private inner class {{ spec.title | caseUcFirst }}WebSocketListener : WebSocketListener() { + private inner class {{ spec.title | caseUcfirst }}WebSocketListener : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { super.onOpen(webSocket, response) @@ -112,14 +132,14 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { Log.i(this@Realtime::class.java.simpleName, "WebSocket message received.") launch(IO) { - val message = try { - Gson().fromJson(text, RealtimeMessage::class.java) - } catch (ex: JsonParseException) { - val error = parseError(text) // TODO: How to propagate this to client? - throw ex - } ?: return@launch - - lastMessage = message + val message = parse(text) + if (message?.channels == null) { + val error = parse(text) ?: return@launch + errorCallbacks.forEach { + it.invoke(error) + } + return@launch + } message.channels.forEachAsync { channel -> channelCallbacks[channel]?.forEachAsync { callback -> @@ -132,7 +152,7 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) Log.i(this@Realtime::class.java.simpleName, "WebSocket closing with code $code because: $reason. Reconnecting in 1 second.") - if (lastMessage?.code == RealtimeCodes.POLICY_VIOLATION.value) { + if (code == RealtimeCodes.POLICY_VIOLATION.value) { return } launch { @@ -147,12 +167,15 @@ class Realtime(client: Client) : BaseService(client), CoroutineScope { t.printStackTrace() } - private fun parseError(text: String) : RealtimeError { + private inline fun parse(text: String): T? { return try { - Gson().fromJson(text, RealtimeError::class.java) + Gson().fromJson(text, T::class.java) + } catch (ex: JsonSyntaxException) { + ex.printStackTrace() + null } catch (ex: JsonParseException) { ex.printStackTrace() - throw ex + null } } } From 6c66937e610e88d5d6a3045492b518fad5653230 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jul 2021 00:14:35 +1200 Subject: [PATCH 11/34] Add unknown error code --- .../src/main/java/io/appwrite/models/RealtimeCodes.kt.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig index f4faa854d..0752d71c7 100644 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig @@ -1,5 +1,6 @@ package {{ sdk.namespace | caseDot }}.models enum class RealtimeCodes(val value: Int) { - POLICY_VIOLATION(1008) + POLICY_VIOLATION(1008), + UNKNOWN_ERROR(-1) } \ No newline at end of file From d676994756ebc6c60aa6b20e8141bb52476d6ff2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 31 Jul 2021 00:53:06 +1200 Subject: [PATCH 12/34] Fix reentry locking on subscribe to defer socket connection, fix unsubscribe --- .../io/appwrite/services/Realtime.kt.twig | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 6bbe2915f..725621c7e 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -26,14 +26,14 @@ class Realtime(client: Client) : Service(client), CoroutineScope { get() = Dispatchers.Main + job private companion object { - private const val debounceDelayMillis = 20 + private const val DEBOUNCE_MILLIS = 1L + private const val MAX_PERMITS = Int.MAX_VALUE private var socket: RealWebSocket? = null private var channelCallbacks = mutableMapOf Unit>>() private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() - private var goingToConnect = false - private var subscribeReentered = false + private var semaphore = Semaphore(MAX_PERMITS) } private fun createSocket() { @@ -87,27 +87,16 @@ class Realtime(client: Client) : Service(client), CoroutineScope { } launch { - goingToConnect = true - delay(debounceDelayMillis.toLong()) - if (subscribeReentered) { - subscribeReentered = false - return@launch - } - - createSocket() - goingToConnect = false - subscribeReentered = false - } + semaphore.tryAcquire() + delay(DEBOUNCE_MILLIS) - return RealtimeSubscription { - unsubscribe(*channels) + if (semaphore.availablePermits == MAX_PERMITS - 1) { + createSocket() + } + semaphore.release() } - } - fun unsubscribe(vararg channels: String) { - channels.forEach { - channelCallbacks.remove(it) - } + return RealtimeSubscription { unsubscribe() } } fun unsubscribe() { @@ -129,7 +118,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) - Log.i(this@Realtime::class.java.simpleName, "WebSocket message received.") + Log.i(this@Realtime::class.java.simpleName, "WebSocket message received: $text") launch(IO) { val message = parse(text) @@ -151,10 +140,11 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - Log.i(this@Realtime::class.java.simpleName, "WebSocket closing with code $code because: $reason. Reconnecting in 1 second.") + Log.i(this@Realtime::class.java.simpleName, "WebSocket closing with code $code because: $reason.") if (code == RealtimeCodes.POLICY_VIOLATION.value) { return } + Log.i(this@Realtime::class.java.simpleName, "Reconnecting in 1 second.") launch { delay(1000) createSocket() From ef06bc6b5bd362ce0379e8084d71890ff643129b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 2 Aug 2021 23:41:05 +1200 Subject: [PATCH 13/34] Add Kotlin realtime files --- src/SDK/Language/Kotlin.php | 44 ++++- .../io/appwrite/services/Realtime.kt.twig | 25 +-- .../main/kotlin/io/appwrite/Client.kt.twig | 34 +++- .../extensions/CollectionExtensions.kt.twig | 12 ++ .../extensions/JsonExtensions.kt.twig | 11 +- .../io/appwrite/models/RealtimeCodes.kt.twig | 6 + .../io/appwrite/models/RealtimeError.kt.twig | 6 + .../io/appwrite/models/RealtimeEvent.kt.twig | 7 + .../appwrite/models/RealtimeMessage.kt.twig | 9 + .../models/RealtimeSubscription.kt.twig | 9 + .../io/appwrite/services/Realtime.kt.twig | 165 ++++++++++++++++++ .../io/appwrite/services/Service.kt.twig | 2 +- .../appwrite/services/ServiceTemplate.kt.twig | 2 +- 13 files changed, 298 insertions(+), 34 deletions(-) create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig create mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 12a3d71f7..be2e02802 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -347,6 +347,12 @@ public function getFiles() 'template' => '/kotlin/src/main/kotlin/io/appwrite/exceptions/Exception.kt.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/CollectionExtensions.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig', + 'minify' => false, + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/JsonExtensions.kt', @@ -361,10 +367,46 @@ public function getFiles() ], [ 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/BaseService.kt', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeError.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeEvent.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeMessage.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeSubscription.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/Service.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/Realtime.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig', + 'minify' => false, + ], [ 'scope' => 'service', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 725621c7e..f29663a0b 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -1,6 +1,5 @@ package {{ sdk.namespace | caseDot }}.services -import android.util.Log import com.google.gson.Gson import com.google.gson.JsonParseException import com.google.gson.JsonSyntaxException @@ -27,13 +26,12 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private companion object { private const val DEBOUNCE_MILLIS = 1L - private const val MAX_PERMITS = Int.MAX_VALUE private var socket: RealWebSocket? = null private var channelCallbacks = mutableMapOf Unit>>() private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() - private var semaphore = Semaphore(MAX_PERMITS) + private var subCallDepth = 0 } private fun createSocket() { @@ -74,10 +72,6 @@ class Realtime(client: Client) : Service(client), CoroutineScope { vararg channels: String, callback: (Any) -> Unit ) : RealtimeSubscription { - if (goingToConnect) { - subscribeReentered = true - } - channels.forEach { if (!channelCallbacks.containsKey(it)) { channelCallbacks[it] = mutableListOf(callback) @@ -87,13 +81,12 @@ class Realtime(client: Client) : Service(client), CoroutineScope { } launch { - semaphore.tryAcquire() + subCallDepth++ delay(DEBOUNCE_MILLIS) - - if (semaphore.availablePermits == MAX_PERMITS - 1) { + if (subCallDepth == 1) { createSocket() } - semaphore.release() + subCallDepth-- } return RealtimeSubscription { unsubscribe() } @@ -113,12 +106,12 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onOpen(webSocket: WebSocket, response: Response) { super.onOpen(webSocket, response) - Log.i(this@Realtime::class.java.simpleName, "WebSocket connected.") + print("WebSocket connected.") } override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) - Log.i(this@Realtime::class.java.simpleName, "WebSocket message received: $text") + print("WebSocket message received: $text") launch(IO) { val message = parse(text) @@ -140,11 +133,11 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - Log.i(this@Realtime::class.java.simpleName, "WebSocket closing with code $code because: $reason.") + print("WebSocket closing with code $code because: $reason.") if (code == RealtimeCodes.POLICY_VIOLATION.value) { return } - Log.i(this@Realtime::class.java.simpleName, "Reconnecting in 1 second.") + print("Reconnecting in 1 second.") launch { delay(1000) createSocket() @@ -153,7 +146,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { super.onFailure(webSocket, t, response) - Log.e(this@Realtime::class.java.simpleName, "WebSocket failure.") + print("WebSocket failure.") t.printStackTrace() } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index ee3f1fea7..fe95104f5 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -2,7 +2,7 @@ package {{ sdk.namespace | caseDot }} import com.google.gson.Gson import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception -import {{ sdk.namespace | caseDot }}.extensions.JsonExtensions.fromJson +import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.models.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,6 +30,7 @@ import kotlin.coroutines.resume class Client @JvmOverloads constructor( var endPoint: String = "https://appwrite.io/v1", + var endPointRealtime: String? = null, private var selfSigned: Boolean = false ) : CoroutineScope { @@ -38,7 +39,7 @@ class Client @JvmOverloads constructor( private val job = Job() - private lateinit var http: OkHttpClient + lateinit var http: OkHttpClient private val headers: MutableMap @@ -127,14 +128,31 @@ class Client @JvmOverloads constructor( } /** - * Set endpoint - * - * @param endpoint - * - * @return this - */ + * Set endpoint and realtime endpoint. + * + * @param endpoint + * + * @return this + */ fun setEndpoint(endPoint: String): Client { this.endPoint = endPoint + + if (this.endPointRealtime == null && endPoint.startsWith("http")) { + this.endPointRealtime = endPoint.replace("http", "ws") + } + + return this + } + + /** + * Set realtime endpoint + * + * @param endpoint + * + * @return this + */ + fun setEndpointRealtime(endPoint: String): Client { + this.endPointRealtime = endPoint return this } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig new file mode 100644 index 000000000..1cae0bea0 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig @@ -0,0 +1,12 @@ +package {{ sdk.namespace | caseDot }}.extensions + +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext + +suspend fun Collection.forEachAsync( + callback: suspend (T) -> Unit +) = withContext(IO) { + map { async { callback.invoke(it) } }.awaitAll() +} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig index e05442224..f7276ee10 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig @@ -2,11 +2,8 @@ package {{ sdk.namespace | caseDot }}.extensions import com.google.gson.Gson -object JsonExtensions { +fun Any.toJson(): String = + Gson().toJson(this) - fun Any.toJson(): String = - Gson().toJson(this) - - fun String.fromJson(clazz: Class): T = - Gson().fromJson(this, clazz) -} \ No newline at end of file +fun String.fromJson(clazz: Class): T = + Gson().fromJson(this, clazz) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig new file mode 100644 index 000000000..0752d71c7 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig @@ -0,0 +1,6 @@ +package {{ sdk.namespace | caseDot }}.models + +enum class RealtimeCodes(val value: Int) { + POLICY_VIOLATION(1008), + UNKNOWN_ERROR(-1) +} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig new file mode 100644 index 000000000..593728d69 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig @@ -0,0 +1,6 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeError( + val code: Int, + val message: String? +) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig new file mode 100644 index 000000000..34ff6e00b --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig @@ -0,0 +1,7 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeEvent( + val project: String, + val permissions: List, + val data: String +) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig new file mode 100644 index 000000000..edb3f956e --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig @@ -0,0 +1,9 @@ +package {{ sdk.namespace | caseDot }}.models + +data class RealtimeMessage( + val code: Int?, + val event: String, + val channels: List, + val timestamp: Long, + val payload: Any +) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig new file mode 100644 index 000000000..3eeeb8b91 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig @@ -0,0 +1,9 @@ +package {{ sdk.namespace | caseDot }}.models + +import java.io.Closeable + +data class RealtimeSubscription( + private val close: () -> Unit +) : Closeable { + override fun close() = close.invoke() +} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig new file mode 100644 index 000000000..f29663a0b --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig @@ -0,0 +1,165 @@ +package {{ sdk.namespace | caseDot }}.services + +import com.google.gson.Gson +import com.google.gson.JsonParseException +import com.google.gson.JsonSyntaxException +import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.extensions.forEachAsync +import {{ sdk.namespace | caseDot }}.models.RealtimeCodes +import {{ sdk.namespace | caseDot }}.models.RealtimeError +import {{ sdk.namespace | caseDot }}.models.RealtimeMessage +import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import okhttp3.* +import okhttp3.internal.concurrent.TaskRunner +import okhttp3.internal.ws.RealWebSocket +import java.util.* +import kotlin.coroutines.CoroutineContext + +class Realtime(client: Client) : Service(client), CoroutineScope { + + private val job = Job() + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + private companion object { + private const val DEBOUNCE_MILLIS = 1L + + private var socket: RealWebSocket? = null + private var channelCallbacks = mutableMapOf Unit>>() + private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() + + private var subCallDepth = 0 + } + + private fun createSocket() { + val queryParamBuilder = StringBuilder() + .append("project=${client.config["project"]}") + + channelCallbacks.keys.forEach { + queryParamBuilder + .append("&channels[]=$it") + } + + val request = Request.Builder() + .url("${client.endPointRealtime}/realtime?$queryParamBuilder") + .build() + + if (socket != null) { + closeSocket() + } + + socket = RealWebSocket( + taskRunner = TaskRunner.INSTANCE, + originalRequest = request, + listener = {{ spec.title | caseUcfirst }}WebSocketListener(), + random = Random(), + pingIntervalMillis = client.http.pingIntervalMillis.toLong(), + extensions = null, + minimumDeflateSize = client.http.minWebSocketMessageToCompress + ) + + socket?.connect(client.http) + } + + private fun closeSocket() { + socket?.close(RealtimeCodes.POLICY_VIOLATION.value, null) + } + + fun subscribe( + vararg channels: String, + callback: (Any) -> Unit + ) : RealtimeSubscription { + channels.forEach { + if (!channelCallbacks.containsKey(it)) { + channelCallbacks[it] = mutableListOf(callback) + return@forEach + } + channelCallbacks[it]?.add(callback) + } + + launch { + subCallDepth++ + delay(DEBOUNCE_MILLIS) + if (subCallDepth == 1) { + createSocket() + } + subCallDepth-- + } + + return RealtimeSubscription { unsubscribe() } + } + + fun unsubscribe() { + channelCallbacks = mutableMapOf() + errorCallbacks = mutableSetOf() + closeSocket() + } + + fun doOnError(callback: (RealtimeError) -> Unit) { + errorCallbacks.add(callback) + } + + private inner class {{ spec.title | caseUcfirst }}WebSocketListener : WebSocketListener() { + + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + print("WebSocket connected.") + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + print("WebSocket message received: $text") + + launch(IO) { + val message = parse(text) + if (message?.channels == null) { + val error = parse(text) ?: return@launch + errorCallbacks.forEach { + it.invoke(error) + } + return@launch + } + + message.channels.forEachAsync { channel -> + channelCallbacks[channel]?.forEachAsync { callback -> + callback.invoke(message.payload) + } + } + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + super.onClosing(webSocket, code, reason) + print("WebSocket closing with code $code because: $reason.") + if (code == RealtimeCodes.POLICY_VIOLATION.value) { + return + } + print("Reconnecting in 1 second.") + launch { + delay(1000) + createSocket() + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + print("WebSocket failure.") + t.printStackTrace() + } + + private inline fun parse(text: String): T? { + return try { + Gson().fromJson(text, T::class.java) + } catch (ex: JsonSyntaxException) { + ex.printStackTrace() + null + } catch (ex: JsonParseException) { + ex.printStackTrace() + null + } + } + } +} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig index 80ceeeffa..a6b5623fc 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig @@ -2,4 +2,4 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Client -abstract class BaseService(private val client: Client) +abstract class Service(val client: Client) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig index d1b3506b9..3f4f8712b 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig @@ -21,7 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl {% endif %} import java.io.File -class {{ service.name | caseUcfirst }}(private val client: Client) : BaseService(client) { +class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { {% for method in service.methods %} /** From 135866fb8e95bd81bc401bf68516e9cc1c0b5a76 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 17 Aug 2021 15:27:03 +0200 Subject: [PATCH 14/34] add tests --- tests/SDKTest.php | 4 ++++ tests/languages/web/index.html | 7 +++++++ tests/languages/web/node.js | 1 + 3 files changed, 12 insertions(+) diff --git a/tests/SDKTest.php b/tests/SDKTest.php index f1bb9ec81..a9b02e3ab 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -130,6 +130,7 @@ class SDKTest extends TestCase 'node' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:bionic node node.js', ], 'supportException' => true, + 'supportRealtime' => true ], 'deno' => [ @@ -316,6 +317,9 @@ public function testHTTPSuccess() $this->assertEquals('Server Error', $output[13] ?? ''); $this->assertEquals('This is a text error', $output[14] ?? ''); } + if ($options['supportRealtime']) { + $this->assertEquals('WS:/v1/realtime:passed', $output[15] ?? ''); + } } } diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 4671d40e5..28496833c 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -19,6 +19,13 @@ // Init SDK let sdk = new Appwrite(); + sdk.setProject('console'); + sdk.setEndpointRealtime('wss://realtime.appwrite.org/v1'); + + sdk.subscribe('tests', event => { + setTimeout(() => console.log(event.payload.response), 5000); + }); + // Foo response = await sdk.foo.get("string", 123, ["string in array"]); console.log(response.result); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index fecf90ebf..2d992d05c 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -68,6 +68,7 @@ async function start() { console.log(error.message); } + console.log('WS:/v1/realtime:passed'); // Skip realtime test on Node.js } start(); \ No newline at end of file From 09fa1f02bb0c5fc220d0a3d51d01dc492db30fd7 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 17 Aug 2021 15:40:41 +0200 Subject: [PATCH 15/34] fix tets --- tests/SDKTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SDKTest.php b/tests/SDKTest.php index a9b02e3ab..f5315b73a 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -317,7 +317,7 @@ public function testHTTPSuccess() $this->assertEquals('Server Error', $output[13] ?? ''); $this->assertEquals('This is a text error', $output[14] ?? ''); } - if ($options['supportRealtime']) { + if ($options['supportRealtime'] ?? false) { $this->assertEquals('WS:/v1/realtime:passed', $output[15] ?? ''); } } From 5e65044a7793cc1b62da847c8b846d8fc68ff971 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 19 Aug 2021 00:32:46 +1200 Subject: [PATCH 16/34] WIP add realtime tests --- tests/SDKTest.php | 2 ++ tests/languages/android/ServiceTest.kt | 20 ++++++++++++++++++-- tests/languages/kotlin/ServiceTest.kt | 25 ++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/SDKTest.php b/tests/SDKTest.php index f5315b73a..31ec7f93f 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -83,6 +83,7 @@ class SDKTest extends TestCase 'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:latest-jdk8 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"', ], 'supportException' => false, + 'supportRealtime' => true, ], 'kotlin' => [ @@ -96,6 +97,7 @@ class SDKTest extends TestCase 'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-alpine sh -c "./gradlew :test -q && cat result.txt"', ], 'supportException' => false, + 'supportRealtime' => true, ], diff --git a/tests/languages/android/ServiceTest.kt b/tests/languages/android/ServiceTest.kt index 8b79a4a03..47ac34a80 100644 --- a/tests/languages/android/ServiceTest.kt +++ b/tests/languages/android/ServiceTest.kt @@ -7,8 +7,14 @@ import io.appwrite.exceptions.AppwriteException import io.appwrite.services.Bar import io.appwrite.services.Foo import io.appwrite.services.General +import io.appwrite.services.Realtime +import io.appwrite.extensions.toJson +import io.appwrite.extensions.fromJson import okhttp3.Response import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,11 +40,18 @@ class ServiceTest { fun test() { val client = Client(ApplicationProvider.getApplicationContext()) + .setEndpointRealtime("wss://realtime.appwrite.org/v1") + .setProject("console") + val foo = Foo(client) val bar = Bar(client) val general = General(client) - client.addHeader("Origin", "http://localhost") - client.setSelfSigned(true) + val realtime = Realtime(client) + var realtimeResponse = "Realtime failed!" + + realtime.subscribe("tests") { + realtimeResponse = it.toJson() + } var response: Response // Foo Tests @@ -90,6 +103,9 @@ class ServiceTest { } catch(e: AppwriteException) { writeToFile(e.message) } + + delay(20000) + writeToFile(realtimeResponse) } } diff --git a/tests/languages/kotlin/ServiceTest.kt b/tests/languages/kotlin/ServiceTest.kt index b1384b094..5d4f3105a 100644 --- a/tests/languages/kotlin/ServiceTest.kt +++ b/tests/languages/kotlin/ServiceTest.kt @@ -4,8 +4,14 @@ import io.appwrite.exceptions.AppwriteException import io.appwrite.services.Bar import io.appwrite.services.Foo import io.appwrite.services.General +import io.appwrite.services.Realtime +import io.appwrite.extensions.toJson +import io.appwrite.extensions.fromJson import okhttp3.Response import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay import org.junit.Before import org.junit.Test import java.io.File @@ -14,6 +20,9 @@ import java.nio.file.Files import java.nio.file.Paths class ServiceTest { + + data class RealtimeResponse(val response: String) + val filename: String = "result.txt" @Before @@ -26,11 +35,18 @@ class ServiceTest { @Throws(IOException::class) fun test() { val client = Client() + .setEndpointRealtime("wss://realtime.appwrite.org/v1") + .setProject("console") + val foo = Foo(client) val bar = Bar(client) val general = General(client) - client.addHeader("Origin", "http://localhost") - client.setSelfSigned(true) + val realtime = Realtime(client) + var realtimeResponse = "Realtime failed!" + + realtime.subscribe("tests") { + realtimeResponse = it.toJson() + } var response: Response // Foo Tests @@ -82,6 +98,9 @@ class ServiceTest { } catch(e: AppwriteException) { writeToFile(e.message) } + + delay(60000) + writeToFile(realtimeResponse) } } @@ -96,7 +115,7 @@ class ServiceTest { writeToFile(map["result"] as String) } - private fun writeToFile(string: String?){ + private fun writeToFile(string: String?) { val text = "${string ?: ""}\n" File("result.txt").appendText(text) } From 12a9fc829b54af0844d84d1e48632a37b5036cc6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 19 Aug 2021 23:16:40 +1200 Subject: [PATCH 17/34] Fix Android + Kotlin coroutine dispatchers for realtime tests --- templates/android/library/build.gradle.twig | 1 + templates/kotlin/build.gradle.twig | 3 +- tests/languages/android/ServiceTest.kt | 50 +++++++++++++-------- tests/languages/kotlin/ServiceTest.kt | 46 +++++++++++-------- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/templates/android/library/build.gradle.twig b/templates/android/library/build.gradle.twig index 724971072..abc32b4af 100644 --- a/templates/android/library/build.gradle.twig +++ b/templates/android/library/build.gradle.twig @@ -74,6 +74,7 @@ dependencies { testImplementation "androidx.test.ext:junit-ktx:1.1.2" testImplementation "androidx.test:core-ktx:1.3.0" testImplementation "org.robolectric:robolectric:4.5.1" + testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1") } apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" \ No newline at end of file diff --git a/templates/kotlin/build.gradle.twig b/templates/kotlin/build.gradle.twig index a0a3dbb71..600752a00 100644 --- a/templates/kotlin/build.gradle.twig +++ b/templates/kotlin/build.gradle.twig @@ -34,7 +34,8 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor") implementation("com.google.code.gson:gson:2.8.5") - testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' + testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") } test { diff --git a/tests/languages/android/ServiceTest.kt b/tests/languages/android/ServiceTest.kt index 47ac34a80..a4fc16104 100644 --- a/tests/languages/android/ServiceTest.kt +++ b/tests/languages/android/ServiceTest.kt @@ -4,17 +4,20 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.gson.Gson import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.fromJson +import io.appwrite.extensions.toJson import io.appwrite.services.Bar import io.appwrite.services.Foo import io.appwrite.services.General import io.appwrite.services.Realtime -import io.appwrite.extensions.toJson -import io.appwrite.extensions.fromJson -import okhttp3.Response -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import okhttp3.Response +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -27,22 +30,31 @@ import java.nio.file.Paths @Config(manifest=Config.NONE) @RunWith(AndroidJUnit4::class) class ServiceTest { - val filename: String = "result.txt" + + private val filename: String = "result.txt" @Before - fun start() { + @ExperimentalCoroutinesApi + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) Files.deleteIfExists(Paths.get(filename)) writeToFile("Test Started") } + @After + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + } + @Test @Throws(IOException::class) fun test() { - val client = Client(ApplicationProvider.getApplicationContext()) .setEndpointRealtime("wss://realtime.appwrite.org/v1") .setProject("console") - + .addHeader("Origin", "http://localhost") + .setSelfSigned(true) val foo = Foo(client) val bar = Bar(client) val general = General(client) @@ -50,12 +62,14 @@ class ServiceTest { var realtimeResponse = "Realtime failed!" realtime.subscribe("tests") { - realtimeResponse = it.toJson() + realtimeResponse = it + .toJson() + .fromJson(Map::class.java)["response"]!! as String } - var response: Response - // Foo Tests runBlocking { + var response: Response + // Foo Tests response = foo.get("string", 123, listOf("string in array")) printResponse(response) response = foo.post("string", 123, listOf("string in array")) @@ -88,23 +102,23 @@ class ServiceTest { try { general.error400() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } try { general.error500() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } try { general.error502() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } - delay(20000) + delay(5000) writeToFile(realtimeResponse) } } @@ -120,7 +134,7 @@ class ServiceTest { writeToFile(map["result"] as String) } - private fun writeToFile(string: String?){ + private fun writeToFile(string: String?) { val text = "${string ?: ""}\n" File("result.txt").appendText(text) } diff --git a/tests/languages/kotlin/ServiceTest.kt b/tests/languages/kotlin/ServiceTest.kt index 5d4f3105a..a1b6607bf 100644 --- a/tests/languages/kotlin/ServiceTest.kt +++ b/tests/languages/kotlin/ServiceTest.kt @@ -1,17 +1,20 @@ import com.google.gson.Gson import io.appwrite.Client import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.fromJson +import io.appwrite.extensions.toJson import io.appwrite.services.Bar import io.appwrite.services.Foo import io.appwrite.services.General import io.appwrite.services.Realtime -import io.appwrite.extensions.toJson -import io.appwrite.extensions.fromJson -import okhttp3.Response -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import okhttp3.Response +import org.junit.After import org.junit.Before import org.junit.Test import java.io.File @@ -21,23 +24,28 @@ import java.nio.file.Paths class ServiceTest { - data class RealtimeResponse(val response: String) - - val filename: String = "result.txt" + private val filename: String = "result.txt" @Before - fun start() { + @ExperimentalCoroutinesApi + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) Files.deleteIfExists(Paths.get(filename)) writeToFile("Test Started") } + @After + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + } + @Test @Throws(IOException::class) fun test() { val client = Client() .setEndpointRealtime("wss://realtime.appwrite.org/v1") .setProject("console") - val foo = Foo(client) val bar = Bar(client) val general = General(client) @@ -45,12 +53,14 @@ class ServiceTest { var realtimeResponse = "Realtime failed!" realtime.subscribe("tests") { - realtimeResponse = it.toJson() + realtimeResponse = it + .toJson() + .fromJson(Map::class.java)["response"]!! as String } - var response: Response - // Foo Tests runBlocking { + var response: Response + // Foo Tests response = foo.get("string", 123, listOf("string in array")) printResponse(response) response = foo.post("string", 123, listOf("string in array")) @@ -83,23 +93,23 @@ class ServiceTest { try { general.error400() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } try { general.error500() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } try { general.error502() - } catch(e: AppwriteException) { + } catch (e: AppwriteException) { writeToFile(e.message) } - delay(60000) + delay(5000) writeToFile(realtimeResponse) } } From ee2a7907dcc68ef68c6148d219e2bd1e5759b067 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 19 Aug 2021 23:40:11 +1200 Subject: [PATCH 18/34] Add Android + Kotlin JSON cast extension to simplify casts to client models --- .../java/io/appwrite/extensions/JsonExtensions.kt.twig | 9 +++++++-- .../kotlin/io/appwrite/extensions/JsonExtensions.kt.twig | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig index a7a258a97..143e19240 100644 --- a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig @@ -2,8 +2,13 @@ package {{ sdk.namespace | caseDot }}.extensions import com.google.gson.Gson +val gson = Gson() + fun Any.toJson(): String = - Gson().toJson(this) + gson.toJson(this) fun String.fromJson(clazz: Class): T = - Gson().fromJson(this, clazz) + gson.fromJson(this, clazz) + +fun Any.jsonCast(to: Class): T = + toJson().fromJson(to) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig index f7276ee10..143e19240 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig @@ -2,8 +2,13 @@ package {{ sdk.namespace | caseDot }}.extensions import com.google.gson.Gson +val gson = Gson() + fun Any.toJson(): String = - Gson().toJson(this) + gson.toJson(this) fun String.fromJson(clazz: Class): T = - Gson().fromJson(this, clazz) \ No newline at end of file + gson.fromJson(this, clazz) + +fun Any.jsonCast(to: Class): T = + toJson().fromJson(to) From ef4fada6c2b441317a78ca45d96be39d62a441b2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Aug 2021 00:31:00 +1200 Subject: [PATCH 19/34] Remove web changes --- tests/SDKTest.php | 1 - tests/languages/web/index.html | 7 ------- tests/languages/web/node.js | 1 - 3 files changed, 9 deletions(-) diff --git a/tests/SDKTest.php b/tests/SDKTest.php index 31ec7f93f..c3014086b 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -132,7 +132,6 @@ class SDKTest extends TestCase 'node' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:bionic node node.js', ], 'supportException' => true, - 'supportRealtime' => true ], 'deno' => [ diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 28496833c..4671d40e5 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -19,13 +19,6 @@ // Init SDK let sdk = new Appwrite(); - sdk.setProject('console'); - sdk.setEndpointRealtime('wss://realtime.appwrite.org/v1'); - - sdk.subscribe('tests', event => { - setTimeout(() => console.log(event.payload.response), 5000); - }); - // Foo response = await sdk.foo.get("string", 123, ["string in array"]); console.log(response.result); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 2d992d05c..fecf90ebf 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -68,7 +68,6 @@ async function start() { console.log(error.message); } - console.log('WS:/v1/realtime:passed'); // Skip realtime test on Node.js } start(); \ No newline at end of file From 38a2f6c2623ba544fe8918a5949a812bcc155dc2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 23 Aug 2021 21:59:50 +1200 Subject: [PATCH 20/34] Review fixes --- .../io/appwrite/services/Realtime.kt.twig | 39 +++++++------------ .../io/appwrite/services/Realtime.kt.twig | 39 +++++++------------ tests/languages/android/ServiceTest.kt | 4 +- tests/languages/kotlin/ServiceTest.kt | 4 +- 4 files changed, 28 insertions(+), 58 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index f29663a0b..0a7ceea38 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -1,10 +1,9 @@ package {{ sdk.namespace | caseDot }}.services -import com.google.gson.Gson -import com.google.gson.JsonParseException -import com.google.gson.JsonSyntaxException import {{ sdk.namespace | caseDot }}.Client import {{ sdk.namespace | caseDot }}.extensions.forEachAsync +import {{ sdk.namespace | caseDot }}.extensions.fromJson +import {{ sdk.namespace | caseDot }}.extensions.jsonCast import {{ sdk.namespace | caseDot }}.models.RealtimeCodes import {{ sdk.namespace | caseDot }}.models.RealtimeError import {{ sdk.namespace | caseDot }}.models.RealtimeMessage @@ -28,7 +27,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private const val DEBOUNCE_MILLIS = 1L private var socket: RealWebSocket? = null - private var channelCallbacks = mutableMapOf Unit>>() + private var channelCallbacks = mutableMapOf) -> Unit>>() private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() private var subCallDepth = 0 @@ -61,7 +60,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { minimumDeflateSize = client.http.minWebSocketMessageToCompress ) - socket?.connect(client.http) + socket!!.connect(client.http) } private fun closeSocket() { @@ -70,7 +69,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { fun subscribe( vararg channels: String, - callback: (Any) -> Unit + callback: (Map<*, *>) -> Unit ) : RealtimeSubscription { channels.forEach { if (!channelCallbacks.containsKey(it)) { @@ -104,14 +103,8 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private inner class {{ spec.title | caseUcfirst }}WebSocketListener : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - super.onOpen(webSocket, response) - print("WebSocket connected.") - } - override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) - print("WebSocket message received: $text") launch(IO) { val message = parse(text) @@ -125,7 +118,9 @@ class Realtime(client: Client) : Service(client), CoroutineScope { message.channels.forEachAsync { channel -> channelCallbacks[channel]?.forEachAsync { callback -> - callback.invoke(message.payload) + callback.invoke( + message.payload.jsonCast(Map::class.java) + ) } } } @@ -133,11 +128,9 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - print("WebSocket closing with code $code because: $reason.") if (code == RealtimeCodes.POLICY_VIOLATION.value) { return } - print("Reconnecting in 1 second.") launch { delay(1000) createSocket() @@ -146,20 +139,14 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { super.onFailure(webSocket, t, response) - print("WebSocket failure.") t.printStackTrace() } - private inline fun parse(text: String): T? { - return try { - Gson().fromJson(text, T::class.java) - } catch (ex: JsonSyntaxException) { - ex.printStackTrace() - null - } catch (ex: JsonParseException) { - ex.printStackTrace() - null - } + private inline fun parse(text: String): T? = try { + text.fromJson(T::class.java) + } catch (ex: Exception) { + ex.printStackTrace() + null } } } \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig index f29663a0b..0a7ceea38 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig @@ -1,10 +1,9 @@ package {{ sdk.namespace | caseDot }}.services -import com.google.gson.Gson -import com.google.gson.JsonParseException -import com.google.gson.JsonSyntaxException import {{ sdk.namespace | caseDot }}.Client import {{ sdk.namespace | caseDot }}.extensions.forEachAsync +import {{ sdk.namespace | caseDot }}.extensions.fromJson +import {{ sdk.namespace | caseDot }}.extensions.jsonCast import {{ sdk.namespace | caseDot }}.models.RealtimeCodes import {{ sdk.namespace | caseDot }}.models.RealtimeError import {{ sdk.namespace | caseDot }}.models.RealtimeMessage @@ -28,7 +27,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private const val DEBOUNCE_MILLIS = 1L private var socket: RealWebSocket? = null - private var channelCallbacks = mutableMapOf Unit>>() + private var channelCallbacks = mutableMapOf) -> Unit>>() private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() private var subCallDepth = 0 @@ -61,7 +60,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { minimumDeflateSize = client.http.minWebSocketMessageToCompress ) - socket?.connect(client.http) + socket!!.connect(client.http) } private fun closeSocket() { @@ -70,7 +69,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { fun subscribe( vararg channels: String, - callback: (Any) -> Unit + callback: (Map<*, *>) -> Unit ) : RealtimeSubscription { channels.forEach { if (!channelCallbacks.containsKey(it)) { @@ -104,14 +103,8 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private inner class {{ spec.title | caseUcfirst }}WebSocketListener : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - super.onOpen(webSocket, response) - print("WebSocket connected.") - } - override fun onMessage(webSocket: WebSocket, text: String) { super.onMessage(webSocket, text) - print("WebSocket message received: $text") launch(IO) { val message = parse(text) @@ -125,7 +118,9 @@ class Realtime(client: Client) : Service(client), CoroutineScope { message.channels.forEachAsync { channel -> channelCallbacks[channel]?.forEachAsync { callback -> - callback.invoke(message.payload) + callback.invoke( + message.payload.jsonCast(Map::class.java) + ) } } } @@ -133,11 +128,9 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - print("WebSocket closing with code $code because: $reason.") if (code == RealtimeCodes.POLICY_VIOLATION.value) { return } - print("Reconnecting in 1 second.") launch { delay(1000) createSocket() @@ -146,20 +139,14 @@ class Realtime(client: Client) : Service(client), CoroutineScope { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { super.onFailure(webSocket, t, response) - print("WebSocket failure.") t.printStackTrace() } - private inline fun parse(text: String): T? { - return try { - Gson().fromJson(text, T::class.java) - } catch (ex: JsonSyntaxException) { - ex.printStackTrace() - null - } catch (ex: JsonParseException) { - ex.printStackTrace() - null - } + private inline fun parse(text: String): T? = try { + text.fromJson(T::class.java) + } catch (ex: Exception) { + ex.printStackTrace() + null } } } \ No newline at end of file diff --git a/tests/languages/android/ServiceTest.kt b/tests/languages/android/ServiceTest.kt index a4fc16104..abf0881cf 100644 --- a/tests/languages/android/ServiceTest.kt +++ b/tests/languages/android/ServiceTest.kt @@ -62,9 +62,7 @@ class ServiceTest { var realtimeResponse = "Realtime failed!" realtime.subscribe("tests") { - realtimeResponse = it - .toJson() - .fromJson(Map::class.java)["response"]!! as String + realtimeResponse = it["response"]!! as String } runBlocking { diff --git a/tests/languages/kotlin/ServiceTest.kt b/tests/languages/kotlin/ServiceTest.kt index a1b6607bf..b9e36ebb0 100644 --- a/tests/languages/kotlin/ServiceTest.kt +++ b/tests/languages/kotlin/ServiceTest.kt @@ -53,9 +53,7 @@ class ServiceTest { var realtimeResponse = "Realtime failed!" realtime.subscribe("tests") { - realtimeResponse = it - .toJson() - .fromJson(Map::class.java)["response"]!! as String + realtimeResponse = it["response"]!! as String } runBlocking { From eb84bc1f7fb79fb1b9228cc3d9d62c4e47bf4c04 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 24 Aug 2021 19:24:46 +1200 Subject: [PATCH 21/34] Use spec exception instead of `RealtimeError` --- src/SDK/Language/Android.php | 6 ------ src/SDK/Language/Kotlin.php | 6 ------ .../main/java/io/appwrite/models/RealtimeError.kt.twig | 6 ------ .../src/main/java/io/appwrite/services/Realtime.kt.twig | 9 +++++---- .../main/kotlin/io/appwrite/services/Realtime.kt.twig | 8 ++++---- 5 files changed, 9 insertions(+), 26 deletions(-) delete mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index ba48b2e77..a590fbc1b 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -146,12 +146,6 @@ public function getFiles() 'template' => '/android/library/src/main/java/io/appwrite/models/Error.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeError.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index be2e02802..ff85409bc 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -365,12 +365,6 @@ public function getFiles() 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/Error.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeError.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig deleted file mode 100644 index 593728d69..000000000 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeError.kt.twig +++ /dev/null @@ -1,6 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeError( - val code: Int, - val message: String? -) \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 0a7ceea38..8dec5fbb2 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -1,11 +1,12 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.extensions.jsonCast import {{ sdk.namespace | caseDot }}.models.RealtimeCodes -import {{ sdk.namespace | caseDot }}.models.RealtimeError + import {{ sdk.namespace | caseDot }}.models.RealtimeMessage import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription import kotlinx.coroutines.* @@ -28,7 +29,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private var socket: RealWebSocket? = null private var channelCallbacks = mutableMapOf) -> Unit>>() - private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() + private var errorCallbacks = mutableSetOf<({{ spec.title | caseUcfirst }}Exception) -> Unit>() private var subCallDepth = 0 } @@ -97,7 +98,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { closeSocket() } - fun doOnError(callback: (RealtimeError) -> Unit) { + fun doOnError(callback: ({{ spec.title | caseUcfirst }}Exception) -> Unit) { errorCallbacks.add(callback) } @@ -109,7 +110,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { launch(IO) { val message = parse(text) if (message?.channels == null) { - val error = parse(text) ?: return@launch + val error = parse<{{ spec.title | caseUcfirst }}Exception>(text) ?: return@launch errorCallbacks.forEach { it.invoke(error) } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig index 0a7ceea38..1995e4caf 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig @@ -1,11 +1,11 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.extensions.jsonCast import {{ sdk.namespace | caseDot }}.models.RealtimeCodes -import {{ sdk.namespace | caseDot }}.models.RealtimeError import {{ sdk.namespace | caseDot }}.models.RealtimeMessage import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription import kotlinx.coroutines.* @@ -28,7 +28,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private var socket: RealWebSocket? = null private var channelCallbacks = mutableMapOf) -> Unit>>() - private var errorCallbacks = mutableSetOf<(RealtimeError) -> Unit>() + private var errorCallbacks = mutableSetOf<({{ spec.title | caseUcfirst }}Exception) -> Unit>() private var subCallDepth = 0 } @@ -97,7 +97,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { closeSocket() } - fun doOnError(callback: (RealtimeError) -> Unit) { + fun doOnError(callback: ({{ spec.title | caseUcfirst }}Exception) -> Unit) { errorCallbacks.add(callback) } @@ -109,7 +109,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { launch(IO) { val message = parse(text) if (message?.channels == null) { - val error = parse(text) ?: return@launch + val error = parse<{{ spec.title | caseUcfirst }}Exception>(text) ?: return@launch errorCallbacks.forEach { it.invoke(error) } From c4fe45d8b7605a31f9a92f0ecd65a26afdda4c73 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 24 Aug 2021 19:26:54 +1200 Subject: [PATCH 22/34] Fix unsubscribe --- .../java/io/appwrite/services/Realtime.kt.twig | 14 +++++++++----- .../kotlin/io/appwrite/services/Realtime.kt.twig | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 8dec5fbb2..f111b3ade 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -89,13 +89,17 @@ class Realtime(client: Client) : Service(client), CoroutineScope { subCallDepth-- } - return RealtimeSubscription { unsubscribe() } + return RealtimeSubscription { unsubscribe(*channels) } } - fun unsubscribe() { - channelCallbacks = mutableMapOf() - errorCallbacks = mutableSetOf() - closeSocket() + fun unsubscribe(vararg channels: String) { + channels.forEach { + channelCallbacks[it] = mutableListOf() + } + if (channelCallbacks.all { it.value.isEmpty() }) { + errorCallbacks = mutableSetOf() + closeSocket() + } } fun doOnError(callback: ({{ spec.title | caseUcfirst }}Exception) -> Unit) { diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig index 1995e4caf..f28d4eaff 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig @@ -88,13 +88,17 @@ class Realtime(client: Client) : Service(client), CoroutineScope { subCallDepth-- } - return RealtimeSubscription { unsubscribe() } + return RealtimeSubscription { unsubscribe(*channels) } } - fun unsubscribe() { - channelCallbacks = mutableMapOf() - errorCallbacks = mutableSetOf() - closeSocket() + fun unsubscribe(vararg channels: String) { + channels.forEach { + channelCallbacks[it] = mutableListOf() + } + if (channelCallbacks.all { it.value.isEmpty() }) { + errorCallbacks = mutableSetOf() + closeSocket() + } } fun doOnError(callback: ({{ spec.title | caseUcfirst }}Exception) -> Unit) { From a3af790d2d08a31d6984cccd1251ad24f0b168f3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 24 Aug 2021 19:44:07 +1200 Subject: [PATCH 23/34] Remove realtime from Kotlin --- src/SDK/Language/Kotlin.php | 36 ---- templates/kotlin/build.gradle.twig | 3 +- .../main/kotlin/io/appwrite/Client.kt.twig | 24 +-- .../extensions/CollectionExtensions.kt.twig | 12 -- .../extensions/JsonExtensions.kt.twig | 14 +- .../io/appwrite/models/RealtimeCodes.kt.twig | 6 - .../io/appwrite/models/RealtimeError.kt.twig | 6 - .../io/appwrite/models/RealtimeEvent.kt.twig | 7 - .../appwrite/models/RealtimeMessage.kt.twig | 9 - .../models/RealtimeSubscription.kt.twig | 9 - .../io/appwrite/services/Realtime.kt.twig | 156 ------------------ .../io/appwrite/services/Service.kt.twig | 2 +- .../appwrite/services/ServiceTemplate.kt.twig | 2 +- tests/SDKTest.php | 3 +- 14 files changed, 13 insertions(+), 276 deletions(-) delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig delete mode 100644 templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index ff85409bc..8360ee3f2 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -347,12 +347,6 @@ public function getFiles() 'template' => '/kotlin/src/main/kotlin/io/appwrite/exceptions/Exception.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/CollectionExtensions.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/JsonExtensions.kt', @@ -365,42 +359,12 @@ public function getFiles() 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/Error.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig', - 'minify' => false, - ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeEvent.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig', - 'minify' => false, - ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeMessage.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig', - 'minify' => false, - ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeSubscription.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/Service.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/Realtime.kt', - 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig', - 'minify' => false, - ], [ 'scope' => 'service', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', diff --git a/templates/kotlin/build.gradle.twig b/templates/kotlin/build.gradle.twig index 600752a00..a0a3dbb71 100644 --- a/templates/kotlin/build.gradle.twig +++ b/templates/kotlin/build.gradle.twig @@ -34,8 +34,7 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor") implementation("com.google.code.gson:gson:2.8.5") - testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' } test { diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index fe95104f5..aa5218314 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -2,7 +2,7 @@ package {{ sdk.namespace | caseDot }} import com.google.gson.Gson import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception -import {{ sdk.namespace | caseDot }}.extensions.fromJson +import {{ sdk.namespace | caseDot }}.extensions.JsonExtensions.fromJson import {{ sdk.namespace | caseDot }}.models.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,7 +30,6 @@ import kotlin.coroutines.resume class Client @JvmOverloads constructor( var endPoint: String = "https://appwrite.io/v1", - var endPointRealtime: String? = null, private var selfSigned: Boolean = false ) : CoroutineScope { @@ -39,7 +38,7 @@ class Client @JvmOverloads constructor( private val job = Job() - lateinit var http: OkHttpClient + private lateinit var http: OkHttpClient private val headers: MutableMap @@ -128,7 +127,7 @@ class Client @JvmOverloads constructor( } /** - * Set endpoint and realtime endpoint. + * Set endpoint. * * @param endpoint * @@ -136,23 +135,6 @@ class Client @JvmOverloads constructor( */ fun setEndpoint(endPoint: String): Client { this.endPoint = endPoint - - if (this.endPointRealtime == null && endPoint.startsWith("http")) { - this.endPointRealtime = endPoint.replace("http", "ws") - } - - return this - } - - /** - * Set realtime endpoint - * - * @param endpoint - * - * @return this - */ - fun setEndpointRealtime(endPoint: String): Client { - this.endPointRealtime = endPoint return this } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig deleted file mode 100644 index 1cae0bea0..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/CollectionExtensions.kt.twig +++ /dev/null @@ -1,12 +0,0 @@ -package {{ sdk.namespace | caseDot }}.extensions - -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext - -suspend fun Collection.forEachAsync( - callback: suspend (T) -> Unit -) = withContext(IO) { - map { async { callback.invoke(it) } }.awaitAll() -} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig index 143e19240..e05442224 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig @@ -2,13 +2,11 @@ package {{ sdk.namespace | caseDot }}.extensions import com.google.gson.Gson -val gson = Gson() +object JsonExtensions { -fun Any.toJson(): String = - gson.toJson(this) + fun Any.toJson(): String = + Gson().toJson(this) -fun String.fromJson(clazz: Class): T = - gson.fromJson(this, clazz) - -fun Any.jsonCast(to: Class): T = - toJson().fromJson(to) + fun String.fromJson(clazz: Class): T = + Gson().fromJson(this, clazz) +} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig deleted file mode 100644 index 0752d71c7..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeCodes.kt.twig +++ /dev/null @@ -1,6 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -enum class RealtimeCodes(val value: Int) { - POLICY_VIOLATION(1008), - UNKNOWN_ERROR(-1) -} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig deleted file mode 100644 index 593728d69..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeError.kt.twig +++ /dev/null @@ -1,6 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeError( - val code: Int, - val message: String? -) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig deleted file mode 100644 index 34ff6e00b..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeEvent.kt.twig +++ /dev/null @@ -1,7 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeEvent( - val project: String, - val permissions: List, - val data: String -) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig deleted file mode 100644 index edb3f956e..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeMessage.kt.twig +++ /dev/null @@ -1,9 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeMessage( - val code: Int?, - val event: String, - val channels: List, - val timestamp: Long, - val payload: Any -) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig deleted file mode 100644 index 3eeeb8b91..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/RealtimeSubscription.kt.twig +++ /dev/null @@ -1,9 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -import java.io.Closeable - -data class RealtimeSubscription( - private val close: () -> Unit -) : Closeable { - override fun close() = close.invoke() -} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig deleted file mode 100644 index f28d4eaff..000000000 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Realtime.kt.twig +++ /dev/null @@ -1,156 +0,0 @@ -package {{ sdk.namespace | caseDot }}.services - -import {{ sdk.namespace | caseDot }}.Client -import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception -import {{ sdk.namespace | caseDot }}.extensions.forEachAsync -import {{ sdk.namespace | caseDot }}.extensions.fromJson -import {{ sdk.namespace | caseDot }}.extensions.jsonCast -import {{ sdk.namespace | caseDot }}.models.RealtimeCodes -import {{ sdk.namespace | caseDot }}.models.RealtimeMessage -import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription -import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers.IO -import okhttp3.* -import okhttp3.internal.concurrent.TaskRunner -import okhttp3.internal.ws.RealWebSocket -import java.util.* -import kotlin.coroutines.CoroutineContext - -class Realtime(client: Client) : Service(client), CoroutineScope { - - private val job = Job() - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job - - private companion object { - private const val DEBOUNCE_MILLIS = 1L - - private var socket: RealWebSocket? = null - private var channelCallbacks = mutableMapOf) -> Unit>>() - private var errorCallbacks = mutableSetOf<({{ spec.title | caseUcfirst }}Exception) -> Unit>() - - private var subCallDepth = 0 - } - - private fun createSocket() { - val queryParamBuilder = StringBuilder() - .append("project=${client.config["project"]}") - - channelCallbacks.keys.forEach { - queryParamBuilder - .append("&channels[]=$it") - } - - val request = Request.Builder() - .url("${client.endPointRealtime}/realtime?$queryParamBuilder") - .build() - - if (socket != null) { - closeSocket() - } - - socket = RealWebSocket( - taskRunner = TaskRunner.INSTANCE, - originalRequest = request, - listener = {{ spec.title | caseUcfirst }}WebSocketListener(), - random = Random(), - pingIntervalMillis = client.http.pingIntervalMillis.toLong(), - extensions = null, - minimumDeflateSize = client.http.minWebSocketMessageToCompress - ) - - socket!!.connect(client.http) - } - - private fun closeSocket() { - socket?.close(RealtimeCodes.POLICY_VIOLATION.value, null) - } - - fun subscribe( - vararg channels: String, - callback: (Map<*, *>) -> Unit - ) : RealtimeSubscription { - channels.forEach { - if (!channelCallbacks.containsKey(it)) { - channelCallbacks[it] = mutableListOf(callback) - return@forEach - } - channelCallbacks[it]?.add(callback) - } - - launch { - subCallDepth++ - delay(DEBOUNCE_MILLIS) - if (subCallDepth == 1) { - createSocket() - } - subCallDepth-- - } - - return RealtimeSubscription { unsubscribe(*channels) } - } - - fun unsubscribe(vararg channels: String) { - channels.forEach { - channelCallbacks[it] = mutableListOf() - } - if (channelCallbacks.all { it.value.isEmpty() }) { - errorCallbacks = mutableSetOf() - closeSocket() - } - } - - fun doOnError(callback: ({{ spec.title | caseUcfirst }}Exception) -> Unit) { - errorCallbacks.add(callback) - } - - private inner class {{ spec.title | caseUcfirst }}WebSocketListener : WebSocketListener() { - - override fun onMessage(webSocket: WebSocket, text: String) { - super.onMessage(webSocket, text) - - launch(IO) { - val message = parse(text) - if (message?.channels == null) { - val error = parse<{{ spec.title | caseUcfirst }}Exception>(text) ?: return@launch - errorCallbacks.forEach { - it.invoke(error) - } - return@launch - } - - message.channels.forEachAsync { channel -> - channelCallbacks[channel]?.forEachAsync { callback -> - callback.invoke( - message.payload.jsonCast(Map::class.java) - ) - } - } - } - } - - override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - super.onClosing(webSocket, code, reason) - if (code == RealtimeCodes.POLICY_VIOLATION.value) { - return - } - launch { - delay(1000) - createSocket() - } - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - super.onFailure(webSocket, t, response) - t.printStackTrace() - } - - private inline fun parse(text: String): T? = try { - text.fromJson(T::class.java) - } catch (ex: Exception) { - ex.printStackTrace() - null - } - } -} \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig index a6b5623fc..80ceeeffa 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig @@ -2,4 +2,4 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Client -abstract class Service(val client: Client) +abstract class BaseService(private val client: Client) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig index b63cb69dd..df0329826 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig @@ -21,7 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl {% endif %} import java.io.File -class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { +class {{ service.name | caseUcfirst }}(private val client: Client) : BaseService(client) { {% for method in service.methods %} /** diff --git a/tests/SDKTest.php b/tests/SDKTest.php index c3014086b..38f605cef 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -96,8 +96,7 @@ class SDKTest extends TestCase 'envs' => [ 'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-alpine sh -c "./gradlew :test -q && cat result.txt"', ], - 'supportException' => false, - 'supportRealtime' => true, + 'supportException' => false ], From a905d20f8454e1f44ce4089dd75f8b3c70732147 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 24 Aug 2021 20:57:07 +1200 Subject: [PATCH 24/34] Fix kotlin removal --- src/SDK/Language/Kotlin.php | 2 +- tests/SDKTest.php | 2 +- tests/languages/kotlin/ServiceTest.kt | 49 ++++++--------------------- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 8360ee3f2..12a3d71f7 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -361,7 +361,7 @@ public function getFiles() ], [ 'scope' => 'default', - 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/Service.kt', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/BaseService.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/Service.kt.twig', 'minify' => false, ], diff --git a/tests/SDKTest.php b/tests/SDKTest.php index 38f605cef..29e770135 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -96,7 +96,7 @@ class SDKTest extends TestCase 'envs' => [ 'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-alpine sh -c "./gradlew :test -q && cat result.txt"', ], - 'supportException' => false + 'supportException' => false, ], diff --git a/tests/languages/kotlin/ServiceTest.kt b/tests/languages/kotlin/ServiceTest.kt index b9e36ebb0..b1384b094 100644 --- a/tests/languages/kotlin/ServiceTest.kt +++ b/tests/languages/kotlin/ServiceTest.kt @@ -1,20 +1,11 @@ import com.google.gson.Gson import io.appwrite.Client import io.appwrite.exceptions.AppwriteException -import io.appwrite.extensions.fromJson -import io.appwrite.extensions.toJson import io.appwrite.services.Bar import io.appwrite.services.Foo import io.appwrite.services.General -import io.appwrite.services.Realtime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain import okhttp3.Response -import org.junit.After +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import java.io.File @@ -23,42 +14,27 @@ import java.nio.file.Files import java.nio.file.Paths class ServiceTest { - - private val filename: String = "result.txt" + val filename: String = "result.txt" @Before - @ExperimentalCoroutinesApi - fun setUp() { - Dispatchers.setMain(Dispatchers.Unconfined) + fun start() { Files.deleteIfExists(Paths.get(filename)) writeToFile("Test Started") } - @After - @ExperimentalCoroutinesApi - fun tearDown() { - Dispatchers.resetMain() - } - @Test @Throws(IOException::class) fun test() { val client = Client() - .setEndpointRealtime("wss://realtime.appwrite.org/v1") - .setProject("console") val foo = Foo(client) val bar = Bar(client) val general = General(client) - val realtime = Realtime(client) - var realtimeResponse = "Realtime failed!" - - realtime.subscribe("tests") { - realtimeResponse = it["response"]!! as String - } + client.addHeader("Origin", "http://localhost") + client.setSelfSigned(true) + var response: Response + // Foo Tests runBlocking { - var response: Response - // Foo Tests response = foo.get("string", 123, listOf("string in array")) printResponse(response) response = foo.post("string", 123, listOf("string in array")) @@ -91,24 +67,21 @@ class ServiceTest { try { general.error400() - } catch (e: AppwriteException) { + } catch(e: AppwriteException) { writeToFile(e.message) } try { general.error500() - } catch (e: AppwriteException) { + } catch(e: AppwriteException) { writeToFile(e.message) } try { general.error502() - } catch (e: AppwriteException) { + } catch(e: AppwriteException) { writeToFile(e.message) } - - delay(5000) - writeToFile(realtimeResponse) } } @@ -123,7 +96,7 @@ class ServiceTest { writeToFile(map["result"] as String) } - private fun writeToFile(string: String?) { + private fun writeToFile(string: String?){ val text = "${string ?: ""}\n" File("result.txt").appendText(text) } From da4fae341beb665396c1bdc8a6939e4dbd6f0aa0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 26 Aug 2021 19:31:13 +1200 Subject: [PATCH 25/34] Remove redundant template --- src/SDK/Language/Android.php | 6 ------ .../src/main/java/io/appwrite/models/RealtimeEvent.kt.twig | 7 ------- 2 files changed, 13 deletions(-) delete mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index a590fbc1b..dd1591176 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -152,12 +152,6 @@ public function getFiles() 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeEvent.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeMessage.kt', diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig deleted file mode 100644 index 34ff6e00b..000000000 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeEvent.kt.twig +++ /dev/null @@ -1,7 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeEvent( - val project: String, - val permissions: List, - val data: String -) \ No newline at end of file From 2641fa2595f3c56d5fb4b7f949b7472212e2602b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 27 Aug 2021 23:39:21 +1200 Subject: [PATCH 26/34] WIP adapt for realtime updates --- src/SDK/Language/Android.php | 6 ------ .../java/io/appwrite/models/RealtimeMessage.kt.twig | 9 --------- .../main/java/io/appwrite/services/Realtime.kt.twig | 12 ++++-------- 3 files changed, 4 insertions(+), 23 deletions(-) delete mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index dd1591176..e47f21f2b 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -152,12 +152,6 @@ public function getFiles() 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig', 'minify' => false, ], - [ - 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeMessage.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig', - 'minify' => false, - ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeSubscription.kt', diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig deleted file mode 100644 index edb3f956e..000000000 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeMessage.kt.twig +++ /dev/null @@ -1,9 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -data class RealtimeMessage( - val code: Int?, - val event: String, - val channels: List, - val timestamp: Long, - val payload: Any -) \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index f111b3ade..5191e684e 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -6,8 +6,6 @@ import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.extensions.jsonCast import {{ sdk.namespace | caseDot }}.models.RealtimeCodes - -import {{ sdk.namespace | caseDot }}.models.RealtimeMessage import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO @@ -112,8 +110,8 @@ class Realtime(client: Client) : Service(client), CoroutineScope { super.onMessage(webSocket, text) launch(IO) { - val message = parse(text) - if (message?.channels == null) { + val message = parse>(text) + if (message == null) { val error = parse<{{ spec.title | caseUcfirst }}Exception>(text) ?: return@launch errorCallbacks.forEach { it.invoke(error) @@ -121,11 +119,9 @@ class Realtime(client: Client) : Service(client), CoroutineScope { return@launch } - message.channels.forEachAsync { channel -> + (message["channels"] as? Collection<*>)?.forEachAsync { channel -> channelCallbacks[channel]?.forEachAsync { callback -> - callback.invoke( - message.payload.jsonCast(Map::class.java) - ) + callback.invoke(message) } } } From c404e46d2d60fd9a4b659bbf9de59e410460a221 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:20:16 +1200 Subject: [PATCH 27/34] Package updates --- templates/android/library/build.gradle.twig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/android/library/build.gradle.twig b/templates/android/library/build.gradle.twig index abc32b4af..d3407200e 100644 --- a/templates/android/library/build.gradle.twig +++ b/templates/android/library/build.gradle.twig @@ -53,8 +53,8 @@ android { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${KotlinCompilerVersion.VERSION}") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3") - api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1") + api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0") api(platform("com.squareup.okhttp3:okhttp-bom:4.9.0")) api("com.squareup.okhttp3:okhttp") @@ -65,14 +65,14 @@ dependencies { implementation("net.gotev:cookie-store:1.3.5") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1") - implementation("androidx.appcompat:appcompat:1.2.0") - implementation("androidx.fragment:fragment-ktx:1.3.2") - implementation("androidx.activity:activity-ktx:1.2.2") + implementation("androidx.appcompat:appcompat:1.3.1") + implementation("androidx.fragment:fragment-ktx:1.3.6") + implementation("androidx.activity:activity-ktx:1.3.1") implementation("androidx.browser:browser:1.3.0") testImplementation 'junit:junit:4.+' - testImplementation "androidx.test.ext:junit-ktx:1.1.2" - testImplementation "androidx.test:core-ktx:1.3.0" + testImplementation "androidx.test.ext:junit-ktx:1.1.3" + testImplementation "androidx.test:core-ktx:1.4.0" testImplementation "org.robolectric:robolectric:4.5.1" testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1") } From 735d16bb432fc344a450cf67b22d80acb0eb866b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:20:37 +1200 Subject: [PATCH 28/34] Add generic event model --- src/SDK/Language/Android.php | 16 ++-------- .../io/appwrite/models/RealtimeCodes.kt.twig | 6 ---- .../io/appwrite/models/RealtimeModels.kt.twig | 31 +++++++++++++++++++ .../models/RealtimeSubscription.kt.twig | 9 ------ 4 files changed, 33 insertions(+), 29 deletions(-) delete mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig create mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeModels.kt.twig delete mode 100644 templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index e47f21f2b..a26c62692 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -142,20 +142,8 @@ public function getFiles() ], [ 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/Error.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/Error.kt.twig', - 'minify' => false, - ], - [ - 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeCodes.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig', - 'minify' => false, - ], - [ - 'scope' => 'default', - 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeSubscription.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/models/RealtimeModels.kt', + 'template' => '/android/library/src/main/java/io/appwrite/models/RealtimeModels.kt.twig', 'minify' => false, ], [ diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig deleted file mode 100644 index 0752d71c7..000000000 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeCodes.kt.twig +++ /dev/null @@ -1,6 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -enum class RealtimeCodes(val value: Int) { - POLICY_VIOLATION(1008), - UNKNOWN_ERROR(-1) -} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeModels.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeModels.kt.twig new file mode 100644 index 000000000..7c02715f5 --- /dev/null +++ b/templates/android/library/src/main/java/io/appwrite/models/RealtimeModels.kt.twig @@ -0,0 +1,31 @@ +package {{ sdk.namespace | caseDot }}.models + +import java.io.Closeable + +data class RealtimeSubscription( + private val close: () -> Unit +) : Closeable { + override fun close() = close.invoke() +} + +data class RealtimeCallback( + val payloadClass: Class<*>, + val callback: (RealtimeResponseEvent<*>) -> Unit +) + +open class RealtimeResponse( + val type: String, + val data: Any +) + +data class RealtimeResponseEvent( + val event: String, + val channels: Collection, + val timestamp: Long, + var payload: T +) + +enum class RealtimeCode(val value: Int) { + POLICY_VIOLATION(1008), + UNKNOWN_ERROR(-1) +} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig deleted file mode 100644 index 3eeeb8b91..000000000 --- a/templates/android/library/src/main/java/io/appwrite/models/RealtimeSubscription.kt.twig +++ /dev/null @@ -1,9 +0,0 @@ -package {{ sdk.namespace | caseDot }}.models - -import java.io.Closeable - -data class RealtimeSubscription( - private val close: () -> Unit -) : Closeable { - override fun close() = close.invoke() -} \ No newline at end of file From 3162fa52a4dfc76fd5af638c3b6506251df244e1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:20:49 +1200 Subject: [PATCH 29/34] Add more JSON helpers --- .../extensions/JsonExtensions.kt.twig | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig index 143e19240..48e536b3a 100644 --- a/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig @@ -10,5 +10,25 @@ fun Any.toJson(): String = fun String.fromJson(clazz: Class): T = gson.fromJson(this, clazz) +inline fun String.fromJson(): T = + gson.fromJson(this, T::class.java) + fun Any.jsonCast(to: Class): T = toJson().fromJson(to) + +inline fun Any.jsonCast(): T = + toJson().fromJson(T::class.java) + +fun Any.tryJsonCast(to: Class): T? = try { + toJson().fromJson(to) +} catch (ex: Exception) { + ex.printStackTrace() + null +} + +inline fun Any.tryJsonCast(): T? = try { + toJson().fromJson(T::class.java) +} catch (ex: Exception) { + ex.printStackTrace() + null +} From d618499db913f3af5652c9b62e958a0a289f806a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:21:08 +1200 Subject: [PATCH 30/34] Remove redundant error class --- .../android/library/src/main/java/io/appwrite/Client.kt.twig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index a27b95f45..65367890d 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -6,7 +6,6 @@ import com.google.gson.Gson import io.appwrite.appwrite.BuildConfig import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.fromJson -import {{ sdk.namespace | caseDot }}.models.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -315,9 +314,9 @@ class Client @JvmOverloads constructor( val contentType: String = response.headers["content-type"] ?: "" val error = if (contentType.contains("application/json", ignoreCase = true)) { - bodyString.fromJson(Error::class.java) + bodyString.fromJson<{{ spec.title | caseUcfirst }}Exception>() } else { - Error(bodyString, response.code) + {{ spec.title | caseUcfirst }}Exception(bodyString, response.code) } it.cancel(AppwriteException( From 5f9c92a498de829300a9e225e4d02491ec24f15e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:21:24 +1200 Subject: [PATCH 31/34] Add typed response/payload support --- .../io/appwrite/services/Realtime.kt.twig | 77 ++++++++++++------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig index 5191e684e..bee4aeff1 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/Realtime.kt.twig @@ -5,11 +5,13 @@ import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Ex import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.extensions.jsonCast -import {{ sdk.namespace | caseDot }}.models.RealtimeCodes -import {{ sdk.namespace | caseDot }}.models.RealtimeSubscription +import {{ sdk.namespace | caseDot }}.models.* import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO -import okhttp3.* +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.ws.RealWebSocket import java.util.* @@ -23,10 +25,13 @@ class Realtime(client: Client) : Service(client), CoroutineScope { get() = Dispatchers.Main + job private companion object { + private const val TYPE_ERROR = "error" + private const val TYPE_EVENT = "event" + private const val DEBOUNCE_MILLIS = 1L private var socket: RealWebSocket? = null - private var channelCallbacks = mutableMapOf) -> Unit>>() + private var channelCallbacks = mutableMapOf>() private var errorCallbacks = mutableSetOf<({{ spec.title | caseUcfirst }}Exception) -> Unit>() private var subCallDepth = 0 @@ -63,19 +68,36 @@ class Realtime(client: Client) : Service(client), CoroutineScope { } private fun closeSocket() { - socket?.close(RealtimeCodes.POLICY_VIOLATION.value, null) + socket?.close(RealtimeCode.POLICY_VIOLATION.value, null) } fun subscribe( vararg channels: String, - callback: (Map<*, *>) -> Unit - ) : RealtimeSubscription { + callback: (RealtimeResponseEvent) -> Unit, + ) = subscribe( + channels = channels, + Any::class.java, + callback + ) + + fun subscribe( + vararg channels: String, + payloadType: Class, + callback: (RealtimeResponseEvent) -> Unit, + ): RealtimeSubscription { channels.forEach { if (!channelCallbacks.containsKey(it)) { - channelCallbacks[it] = mutableListOf(callback) + channelCallbacks[it] = mutableListOf( + RealtimeCallback( + payloadType, + callback as (RealtimeResponseEvent<*>) -> Unit + ) + ) return@forEach } - channelCallbacks[it]?.add(callback) + channelCallbacks[it]?.add( + RealtimeCallback(payloadType, callback as (RealtimeResponseEvent<*>) -> Unit) + ) } launch { @@ -110,26 +132,32 @@ class Realtime(client: Client) : Service(client), CoroutineScope { super.onMessage(webSocket, text) launch(IO) { - val message = parse>(text) - if (message == null) { - val error = parse<{{ spec.title | caseUcfirst }}Exception>(text) ?: return@launch - errorCallbacks.forEach { - it.invoke(error) - } - return@launch + val message = text.fromJson() + when (message.type) { + TYPE_ERROR -> handleResponseError(message) + TYPE_EVENT -> handleResponseEvent(message) } + } + } - (message["channels"] as? Collection<*>)?.forEachAsync { channel -> - channelCallbacks[channel]?.forEachAsync { callback -> - callback.invoke(message) - } + private fun handleResponseError(message: RealtimeResponse) { + val error = message.data.jsonCast<{{ spec.title | caseUcfirst }}Exception>() + errorCallbacks.forEach { it.invoke(error) } + } + + private suspend fun handleResponseEvent(message: RealtimeResponse) { + val event = message.data.jsonCast>() + event.channels.forEachAsync { channel -> + channelCallbacks[channel]?.forEachAsync { callbackWrapper -> + event.payload = event.payload.jsonCast(callbackWrapper.payloadClass) + callbackWrapper.callback.invoke(event) } } } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - if (code == RealtimeCodes.POLICY_VIOLATION.value) { + if (code == RealtimeCode.POLICY_VIOLATION.value) { return } launch { @@ -142,12 +170,5 @@ class Realtime(client: Client) : Service(client), CoroutineScope { super.onFailure(webSocket, t, response) t.printStackTrace() } - - private inline fun parse(text: String): T? = try { - text.fromJson(T::class.java) - } catch (ex: Exception) { - ex.printStackTrace() - null - } } } \ No newline at end of file From 8a551f22e838970863fdd68f68942c0018f70755 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 30 Aug 2021 23:21:49 +1200 Subject: [PATCH 32/34] Upload test to support realtime payload typing --- tests/languages/android/ServiceTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/languages/android/ServiceTest.kt b/tests/languages/android/ServiceTest.kt index abf0881cf..d3b5d942d 100644 --- a/tests/languages/android/ServiceTest.kt +++ b/tests/languages/android/ServiceTest.kt @@ -27,6 +27,8 @@ import java.io.IOException import java.nio.file.Files import java.nio.file.Paths +data class TestPayload(val response: String) + @Config(manifest=Config.NONE) @RunWith(AndroidJUnit4::class) class ServiceTest { @@ -61,8 +63,8 @@ class ServiceTest { val realtime = Realtime(client) var realtimeResponse = "Realtime failed!" - realtime.subscribe("tests") { - realtimeResponse = it["response"]!! as String + realtime.subscribe("tests", payloadType = TestPayload::class.java) { + realtimeResponse = it.payload.response } runBlocking { From 2ca9c25b374c7681e90b2e86c0ebb0837753510e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 31 Aug 2021 20:20:18 +1200 Subject: [PATCH 33/34] Fix realtime URL parsing for more than one occurence of `http` --- .../android/library/src/main/java/io/appwrite/Client.kt.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 65367890d..8ec8dc393 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -161,7 +161,7 @@ class Client @JvmOverloads constructor( this.endPoint = endPoint if (this.endPointRealtime == null && endPoint.startsWith("http")) { - this.endPointRealtime = endPoint.replace("http", "ws") + this.endPointRealtime = endPoint.replaceFirst("http", "ws") } return this From 0857e5cf2a4b28e838bb43fe60847cc87a9ddf9c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Sep 2021 00:37:33 +1200 Subject: [PATCH 34/34] Set publish version from release tag --- templates/android/.github/workflows/publish.yml | 3 ++- templates/android/library/build.gradle.twig | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/android/.github/workflows/publish.yml b/templates/android/.github/workflows/publish.yml index b777478bf..20b880d0c 100644 --- a/templates/android/.github/workflows/publish.yml +++ b/templates/android/.github/workflows/publish.yml @@ -50,4 +50,5 @@ jobs: SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} - SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} \ No newline at end of file + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SDK_VERSION: ${{ github.event.release.tag_name }} \ No newline at end of file diff --git a/templates/android/library/build.gradle.twig b/templates/android/library/build.gradle.twig index d3407200e..8557d14d2 100644 --- a/templates/android/library/build.gradle.twig +++ b/templates/android/library/build.gradle.twig @@ -7,7 +7,7 @@ plugins { ext { PUBLISH_GROUP_ID = '{{ sdk.namespace | caseDot }}' PUBLISH_ARTIFACT_ID = '{{ sdk.gitRepoName | caseDash }}' - PUBLISH_VERSION = '{{ sdk.version }}' + PUBLISH_VERSION = System.getenv('SDK_VERSION') POM_URL = 'https://github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}' POM_SCM_URL = 'https://github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}' POM_ISSUE_URL = 'https://github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}/issues'