From 3ff496eeceb4f30e6ac07b87777c7f0b2aa6585d Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 13 Aug 2026 12:11:06 +0100 Subject: [PATCH] pubsub: add device and server packages per PDR-091 Under MAU pricing Ably needs to know whether traffic comes from an end-user device or from a customer's own backend. PDR-091 showed that inferring this from the auth type is wrong often enough to matter, so make the side structural instead: the artifact you depend on and the factory you call state it. Two additive artifacts, each re-exporting the core via `api` so that every io.ably.lib type stays importable: - io.ably.pubsub:server, a JVM jar over ably-java, exposing PubSub.httpClientBuilder() and PubSub.realtimeClientBuilder() - io.ably.pubsub:device, a Kotlin Multiplatform module over ably-android and ably-java, exposing PubSub.clientBuilder(). Targeting the JVM as well as Android means desktop applications can declare the device side too. Each builder exposes one method per ClientOptions property and assembles the whole ClientOptions itself; the HTTP builder omits the realtime-only options, which an AblyRest would ignore. The existing AblyRest and AblyRealtime constructors are deprecated in place, naming their replacement, and remain fully functional. Nothing changes on the wire yet, the server/client specific changes will be in the next commit Co-Authored-By: Claude Opus 5 (1M context) --- .../io/ably/lib/push/ActivationContext.java | 2 + .../main/java/io/ably/lib/rest/AblyRest.java | 8 + build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + .../main/java/io/ably/lib/rest/AblyRest.java | 8 + .../io/ably/lib/realtime/AblyRealtime.java | 10 + pubsub-device/build.gradle.kts | 89 +++ pubsub-device/gradle.properties | 4 + .../src/androidMain/AndroidManifest.xml | 2 + .../io/ably/pubsub/device/PubSubDevice.kt | 423 ++++++++++++ pubsub-server/build.gradle.kts | 25 + pubsub-server/gradle.properties | 5 + .../io/ably/pubsub/server/PubSubServer.java | 632 ++++++++++++++++++ settings.gradle.kts | 2 + 14 files changed, 1212 insertions(+) create mode 100644 pubsub-device/build.gradle.kts create mode 100644 pubsub-device/gradle.properties create mode 100644 pubsub-device/src/androidMain/AndroidManifest.xml create mode 100644 pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt create mode 100644 pubsub-server/build.gradle.kts create mode 100644 pubsub-server/gradle.properties create mode 100644 pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java diff --git a/android/src/main/java/io/ably/lib/push/ActivationContext.java b/android/src/main/java/io/ably/lib/push/ActivationContext.java index addb7d4eb..849631248 100644 --- a/android/src/main/java/io/ably/lib/push/ActivationContext.java +++ b/android/src/main/java/io/ably/lib/push/ActivationContext.java @@ -60,6 +60,7 @@ public void setAbly(AblyRest ably) { this.clientId = ably.auth.clientId; } + @SuppressWarnings("deprecation") // internal push-registration client, not an application entry point AblyRest getAbly() throws AblyException { if(ably != null) { Log.v(TAG, "getAbly(): returning existing Ably instance"); @@ -84,6 +85,7 @@ AblyRest getAbly() throws AblyException { * @return AblyRest instance with device identity token auth. We use this instance to perform * deregistration calls in push activation flow. */ + @SuppressWarnings("deprecation") // internal push-registration client, not an application entry point AblyRest getDeviceIdentityTokenBasedAblyClient(String deviceIdentityToken) throws AblyException { ClientOptions clientOptions = ably.options.copy(); clientOptions.clearAuthOptions(); diff --git a/android/src/main/java/io/ably/lib/rest/AblyRest.java b/android/src/main/java/io/ably/lib/rest/AblyRest.java index 7f04feb2a..fd853ced0 100644 --- a/android/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java @@ -20,7 +20,11 @@ public class AblyRest extends AblyBase { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(String key) throws AblyException { super(key, new AndroidPlatformAgentProvider()); } @@ -31,7 +35,11 @@ public AblyRest(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object to configure the client connection to Ably. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(ClientOptions options) throws AblyException { super(options, new AndroidPlatformAgentProvider()); } diff --git a/build.gradle.kts b/build.gradle.kts index a98b165b6..308f58e4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.test.retry) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.compose) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e935db93..8664438d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,4 +91,5 @@ lombok = { id = "io.freefair.lombok", version.ref = "lombok" } test-retry = { id = "org.gradle.test-retry", version.ref = "test-retry" } android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/java/src/main/java/io/ably/lib/rest/AblyRest.java b/java/src/main/java/io/ably/lib/rest/AblyRest.java index 7ab6a3390..682c019ff 100644 --- a/java/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/java/src/main/java/io/ably/lib/rest/AblyRest.java @@ -17,7 +17,11 @@ public class AblyRest extends AblyBase { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.server.PubSubServer#httpClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(String key) throws AblyException { super(key, new JavaPlatformAgentProvider()); } @@ -28,7 +32,11 @@ public AblyRest(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object to configure the client connection to Ably. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.server.PubSubServer#httpClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(ClientOptions options) throws AblyException { super(options, new JavaPlatformAgentProvider()); } diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java index b991ed63b..efd8bcad6 100644 --- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java +++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java @@ -56,7 +56,12 @@ public class AblyRealtime extends AblyRest { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact if this code runs on an end-user device, or + * {@code io.ably.pubsub.server.PubSubServer#realtimeClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact if it runs on infrastructure you control. */ + @Deprecated public AblyRealtime(String key) throws AblyException { this(new ClientOptions(key)); } @@ -67,7 +72,12 @@ public AblyRealtime(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact if this code runs on an end-user device, or + * {@code io.ably.pubsub.server.PubSubServer#realtimeClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact if it runs on infrastructure you control. */ + @Deprecated public AblyRealtime(ClientOptions options) throws AblyException { super(options); final InternalChannels channels = new InternalChannels(); diff --git a/pubsub-device/build.gradle.kts b/pubsub-device/build.gradle.kts new file mode 100644 index 000000000..9d6753740 --- /dev/null +++ b/pubsub-device/build.gradle.kts @@ -0,0 +1,89 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinMultiplatform +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.maven.publish) +} + +kotlin { + explicitApi() + + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + publishLibraryVariants("release") + } + + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + } + + sourceSets { + commonMain.dependencies { + /* + * The two platform artifacts publish the same io.ably.lib.* types, so common code + * compiles against either one. ably-java is the arbitrary pick; each target below + * brings the real one. + */ + compileOnly(project(":java")) + } + androidMain.dependencies { + api(project(":android")) + } + jvmMain.dependencies { + api(project(":java")) + } + commonTest.dependencies { + compileOnly(project(":java")) + implementation(kotlin("test")) + } + } +} + +android { + namespace = "io.ably.pubsub.device" + compileSdk = 34 + + defaultConfig { + minSdk = 19 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + + lint { + abortOnError = false + } + + testOptions { + targetSdk = 34 + /* + * AndroidPlatformAgentProvider reads android.os.Build.VERSION.SDK_INT, which is an unmocked + * stub in local unit tests. Without this it throws instead of returning a default. + */ + unitTests.isReturnDefaultValues = true + } +} + +mavenPublishing { + configure(KotlinMultiplatform(javadocJar = JavadocJar.Empty(), androidVariantsToPublish = listOf("release"))) +} + +/* check.yml invokes `runUnitTests` unqualified across all projects. */ +tasks.register("runUnitTests") { + dependsOn("jvmTest", "testDebugUnitTest") +} diff --git a/pubsub-device/gradle.properties b/pubsub-device/gradle.properties new file mode 100644 index 000000000..e251157c9 --- /dev/null +++ b/pubsub-device/gradle.properties @@ -0,0 +1,4 @@ +GROUP=io.ably.pubsub +POM_ARTIFACT_ID=device +POM_NAME=Ably Pub/Sub SDK for devices +POM_DESCRIPTION=Ably Pub/Sub SDK for applications running on end-user devices, for Android and the JVM. diff --git a/pubsub-device/src/androidMain/AndroidManifest.xml b/pubsub-device/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..a2f47b605 --- /dev/null +++ b/pubsub-device/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt b/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt new file mode 100644 index 000000000..31f40256f --- /dev/null +++ b/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt @@ -0,0 +1,423 @@ +package io.ably.pubsub.device + +import io.ably.lib.push.Storage +import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.rest.Auth +import io.ably.lib.types.ClientOptions +import io.ably.lib.types.Param +import io.ably.lib.types.ProxyOptions +import io.ably.lib.util.Log + +/** + * Entry point for the Ably Pub/Sub SDK for devices: applications running on end-user devices, whose + * connections are counted on accounts with monthly-active-user billing. + * + * Clients built here are the same [AblyRealtime] objects the core SDK has always returned, and + * behave identically. What the artifact adds is the choice itself: the dependency you declare and + * the factory you call state which side of the connection your code runs on, rather than leaving it + * to be inferred. + * + * If your code runs on infrastructure you control, use the `io.ably.pubsub:server` artifact instead. + * + * There is a single door because there is a single choice to make. Connectionless operations — + * history, presence queries, token requests, `request()` and `batchPublish()` — are all available on + * the client this returns. + * + * ``` + * val client = PubSubDevice.clientBuilder() + * .key("xVLyHw.MHOCLg:...") + * .clientId("bob") + * .build() + * ``` + */ +public object PubSubDevice { + + /** + * Creates a builder for a device client. It exposes one method per [ClientOptions] property. + * + * @return a new builder. + */ + @JvmStatic + public fun clientBuilder(): ClientBuilder = ClientBuilder() + + /** + * Builds a device client. Obtain one from [PubSubDevice.clientBuilder]. + */ + public class ClientBuilder internal constructor() { + + /** Accumulates the calls made on this builder; handed to the client as-is by [build]. */ + private val options = ClientOptions() + + /** + * Sets [Auth.AuthOptions.authCallback]. + * + * @param authCallback the value to set. + * @return this builder. + */ + public fun authCallback(authCallback: Auth.TokenCallback): ClientBuilder = apply { options.authCallback = authCallback } + + /** + * Sets [Auth.AuthOptions.authUrl]. + * + * @param authUrl the value to set. + * @return this builder. + */ + public fun authUrl(authUrl: String): ClientBuilder = apply { options.authUrl = authUrl } + + /** + * Sets [Auth.AuthOptions.authMethod]. + * + * @param authMethod the value to set. + * @return this builder. + */ + public fun authMethod(authMethod: String): ClientBuilder = apply { options.authMethod = authMethod } + + /** + * Sets [Auth.AuthOptions.key]. + * + * @param key the value to set. + * @return this builder. + */ + public fun key(key: String): ClientBuilder = apply { options.key = key } + + /** + * Sets [Auth.AuthOptions.token]. + * + * @param token the value to set. + * @return this builder. + */ + public fun token(token: String): ClientBuilder = apply { options.token = token } + + /** + * Sets [Auth.AuthOptions.tokenDetails]. + * + * @param tokenDetails the value to set. + * @return this builder. + */ + public fun tokenDetails(tokenDetails: Auth.TokenDetails): ClientBuilder = apply { options.tokenDetails = tokenDetails } + + /** + * Sets [Auth.AuthOptions.authHeaders]. + * + * @param authHeaders the value to set. + * @return this builder. + */ + public fun authHeaders(authHeaders: Array): ClientBuilder = apply { options.authHeaders = authHeaders } + + /** + * Sets [Auth.AuthOptions.authParams]. + * + * @param authParams the value to set. + * @return this builder. + */ + public fun authParams(authParams: Array): ClientBuilder = apply { options.authParams = authParams } + + /** + * Sets [Auth.AuthOptions.queryTime]. + * + * @param queryTime the value to set. + * @return this builder. + */ + public fun queryTime(queryTime: Boolean): ClientBuilder = apply { options.queryTime = queryTime } + + /** + * Sets [Auth.AuthOptions.useTokenAuth]. + * + * @param useTokenAuth the value to set. + * @return this builder. + */ + public fun useTokenAuth(useTokenAuth: Boolean): ClientBuilder = apply { options.useTokenAuth = useTokenAuth } + + /** + * Sets [ClientOptions.clientId]. + * + * @param clientId the value to set. + * @return this builder. + */ + public fun clientId(clientId: String): ClientBuilder = apply { options.clientId = clientId } + + /** + * Sets [ClientOptions.logLevel]. + * + * @param logLevel the value to set. + * @return this builder. + */ + public fun logLevel(logLevel: Int): ClientBuilder = apply { options.logLevel = logLevel } + + /** + * Sets [ClientOptions.logHandler]. + * + * @param logHandler the value to set. + * @return this builder. + */ + public fun logHandler(logHandler: Log.LogHandler): ClientBuilder = apply { options.logHandler = logHandler } + + /** + * Sets [ClientOptions.tls]. + * + * @param tls the value to set. + * @return this builder. + */ + public fun tls(tls: Boolean): ClientBuilder = apply { options.tls = tls } + + /** + * Sets [ClientOptions.headers]. + * + * @param headers the value to set. + * @return this builder. + */ + public fun headers(headers: Map): ClientBuilder = apply { options.headers = headers } + + /** + * Sets [ClientOptions.restHost]. + * + * @param restHost the value to set. + * @return this builder. + */ + public fun restHost(restHost: String): ClientBuilder = apply { options.restHost = restHost } + + /** + * Sets [ClientOptions.port]. + * + * @param port the value to set. + * @return this builder. + */ + public fun port(port: Int): ClientBuilder = apply { options.port = port } + + /** + * Sets [ClientOptions.tlsPort]. + * + * @param tlsPort the value to set. + * @return this builder. + */ + public fun tlsPort(tlsPort: Int): ClientBuilder = apply { options.tlsPort = tlsPort } + + /** + * Sets [ClientOptions.useBinaryProtocol]. + * + * @param useBinaryProtocol the value to set. + * @return this builder. + */ + public fun useBinaryProtocol(useBinaryProtocol: Boolean): ClientBuilder = apply { options.useBinaryProtocol = useBinaryProtocol } + + /** + * Sets [ClientOptions.proxy]. + * + * @param proxy the value to set. + * @return this builder. + */ + public fun proxy(proxy: ProxyOptions): ClientBuilder = apply { options.proxy = proxy } + + /** + * Sets [ClientOptions.environment]. + * + * @param environment the value to set. + * @return this builder. + */ + public fun environment(environment: String): ClientBuilder = apply { options.environment = environment } + + /** + * Sets [ClientOptions.idempotentRestPublishing]. + * + * @param idempotentRestPublishing the value to set. + * @return this builder. + */ + public fun idempotentRestPublishing(idempotentRestPublishing: Boolean): ClientBuilder = apply { options.idempotentRestPublishing = idempotentRestPublishing } + + /** + * Sets [ClientOptions.httpOpenTimeout]. + * + * @param httpOpenTimeout the value to set. + * @return this builder. + */ + public fun httpOpenTimeout(httpOpenTimeout: Int): ClientBuilder = apply { options.httpOpenTimeout = httpOpenTimeout } + + /** + * Sets [ClientOptions.httpRequestTimeout]. + * + * @param httpRequestTimeout the value to set. + * @return this builder. + */ + public fun httpRequestTimeout(httpRequestTimeout: Int): ClientBuilder = apply { options.httpRequestTimeout = httpRequestTimeout } + + /** + * Sets [ClientOptions.httpMaxRetryDuration]. + * + * @param httpMaxRetryDuration the value to set. + * @return this builder. + */ + public fun httpMaxRetryDuration(httpMaxRetryDuration: Int): ClientBuilder = apply { options.httpMaxRetryDuration = httpMaxRetryDuration } + + /** + * Sets [ClientOptions.httpMaxRetryCount]. + * + * @param httpMaxRetryCount the value to set. + * @return this builder. + */ + public fun httpMaxRetryCount(httpMaxRetryCount: Int): ClientBuilder = apply { options.httpMaxRetryCount = httpMaxRetryCount } + + /** + * Sets [ClientOptions.fallbackHosts]. + * + * @param fallbackHosts the value to set. + * @return this builder. + */ + public fun fallbackHosts(fallbackHosts: Array): ClientBuilder = apply { options.fallbackHosts = fallbackHosts } + + /** + * Sets [ClientOptions.fallbackHostsUseDefault]. + * + * @param fallbackHostsUseDefault the value to set. + * @return this builder. + */ + @Deprecated("Deprecated on ClientOptions itself; use fallbackHosts to supply custom hosts.") + @Suppress("DEPRECATION") + public fun fallbackHostsUseDefault(fallbackHostsUseDefault: Boolean): ClientBuilder = apply { options.fallbackHostsUseDefault = fallbackHostsUseDefault } + + /** + * Sets [ClientOptions.fallbackRetryTimeout]. + * + * @param fallbackRetryTimeout the value to set. + * @return this builder. + */ + public fun fallbackRetryTimeout(fallbackRetryTimeout: Long): ClientBuilder = apply { options.fallbackRetryTimeout = fallbackRetryTimeout } + + /** + * Sets [ClientOptions.defaultTokenParams]. + * + * @param defaultTokenParams the value to set. + * @return this builder. + */ + public fun defaultTokenParams(defaultTokenParams: Auth.TokenParams): ClientBuilder = apply { options.defaultTokenParams = defaultTokenParams } + + /** + * Sets [ClientOptions.asyncHttpThreadpoolSize]. + * + * @param asyncHttpThreadpoolSize the value to set. + * @return this builder. + */ + public fun asyncHttpThreadpoolSize(asyncHttpThreadpoolSize: Int): ClientBuilder = apply { options.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize } + + /** + * Sets [ClientOptions.pushFullWait]. + * + * @param pushFullWait the value to set. + * @return this builder. + */ + public fun pushFullWait(pushFullWait: Boolean): ClientBuilder = apply { options.pushFullWait = pushFullWait } + + /** + * Sets [ClientOptions.localStorage]. + * + * @param localStorage the value to set. + * @return this builder. + */ + public fun localStorage(localStorage: Storage): ClientBuilder = apply { options.localStorage = localStorage } + + /** + * Sets [ClientOptions.addRequestIds]. + * + * @param addRequestIds the value to set. + * @return this builder. + */ + public fun addRequestIds(addRequestIds: Boolean): ClientBuilder = apply { options.addRequestIds = addRequestIds } + + /** + * Sets [ClientOptions.agents]. + * + * @param agents the value to set. + * @return this builder. + */ + public fun agents(agents: Map): ClientBuilder = apply { options.agents = agents } + + /** + * Sets [ClientOptions.realtimeHost]. + * + * @param realtimeHost the value to set. + * @return this builder. + */ + public fun realtimeHost(realtimeHost: String): ClientBuilder = apply { options.realtimeHost = realtimeHost } + + /** + * Sets [ClientOptions.autoConnect]. + * + * @param autoConnect the value to set. + * @return this builder. + */ + public fun autoConnect(autoConnect: Boolean): ClientBuilder = apply { options.autoConnect = autoConnect } + + /** + * Sets [ClientOptions.queueMessages]. + * + * @param queueMessages the value to set. + * @return this builder. + */ + public fun queueMessages(queueMessages: Boolean): ClientBuilder = apply { options.queueMessages = queueMessages } + + /** + * Sets [ClientOptions.echoMessages]. + * + * @param echoMessages the value to set. + * @return this builder. + */ + public fun echoMessages(echoMessages: Boolean): ClientBuilder = apply { options.echoMessages = echoMessages } + + /** + * Sets [ClientOptions.recover]. + * + * @param recover the value to set. + * @return this builder. + */ + public fun recover(recover: String): ClientBuilder = apply { options.recover = recover } + + /** + * Sets [ClientOptions.realtimeRequestTimeout]. + * + * @param realtimeRequestTimeout the value to set. + * @return this builder. + */ + public fun realtimeRequestTimeout(realtimeRequestTimeout: Long): ClientBuilder = apply { options.realtimeRequestTimeout = realtimeRequestTimeout } + + /** + * Sets [ClientOptions.disconnectedRetryTimeout]. + * + * @param disconnectedRetryTimeout the value to set. + * @return this builder. + */ + public fun disconnectedRetryTimeout(disconnectedRetryTimeout: Long): ClientBuilder = apply { options.disconnectedRetryTimeout = disconnectedRetryTimeout } + + /** + * Sets [ClientOptions.suspendedRetryTimeout]. + * + * @param suspendedRetryTimeout the value to set. + * @return this builder. + */ + public fun suspendedRetryTimeout(suspendedRetryTimeout: Long): ClientBuilder = apply { options.suspendedRetryTimeout = suspendedRetryTimeout } + + /** + * Sets [ClientOptions.channelRetryTimeout]. + * + * @param channelRetryTimeout the value to set. + * @return this builder. + */ + public fun channelRetryTimeout(channelRetryTimeout: Int): ClientBuilder = apply { options.channelRetryTimeout = channelRetryTimeout } + + /** + * Sets [ClientOptions.transportParams]. + * + * @param transportParams the value to set. + * @return this builder. + */ + public fun transportParams(transportParams: Array): ClientBuilder = apply { options.transportParams = transportParams } + + /** + * Builds the client, which connects immediately unless [autoConnect] was set to false. + * + * @return an [AblyRealtime]. + * @throws io.ably.lib.types.AblyException if the options are invalid, for example if no + * authentication parameters were supplied. + */ + @Suppress("DEPRECATION") // this factory is the replacement for that constructor + public fun build(): AblyRealtime = AblyRealtime(options) + } +} diff --git a/pubsub-server/build.gradle.kts b/pubsub-server/build.gradle.kts new file mode 100644 index 000000000..04b83afdf --- /dev/null +++ b/pubsub-server/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.maven.publish) + checkstyle + `java-library` +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + /* + * api, not implementation: the factories return io.ably.lib.rest.AblyRest and + * io.ably.lib.realtime.AblyRealtime, and consumers configure them with io.ably.lib.types.*, + * so the core is part of this module's compile-time ABI. + */ + api(project(":java")) + testImplementation(libs.bundles.tests) +} + +tasks.register("runUnitTests") { + beforeTest(closureOf { logger.lifecycle("-> $this") }) + outputs.upToDateWhen { false } +} diff --git a/pubsub-server/gradle.properties b/pubsub-server/gradle.properties new file mode 100644 index 000000000..5abdf4810 --- /dev/null +++ b/pubsub-server/gradle.properties @@ -0,0 +1,5 @@ +GROUP=io.ably.pubsub +POM_ARTIFACT_ID=server +POM_NAME=Ably Pub/Sub SDK for servers +POM_DESCRIPTION=Ably Pub/Sub SDK for server-side JVM applications. +POM_PACKAGING=jar diff --git a/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java b/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java new file mode 100644 index 000000000..ce709a5f5 --- /dev/null +++ b/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java @@ -0,0 +1,632 @@ +package io.ably.pubsub.server; + +import io.ably.lib.push.Storage; +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.rest.AblyRest; +import io.ably.lib.rest.Auth; +import io.ably.lib.types.AblyException; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.Param; +import io.ably.lib.types.ProxyOptions; +import io.ably.lib.util.Log.LogHandler; + +import java.util.Map; + +/** + * Entry point for the Ably Pub/Sub SDK for servers: applications running on infrastructure you + * control, whose traffic is exempt from monthly-active-user billing. + *

+ * Clients built here are the same {@link AblyRest} and {@link AblyRealtime} objects the core SDK + * has always returned, and behave identically. What the artifact adds is the choice itself: the + * dependency you declare and the factory you call state which side of the connection your code runs + * on, rather than leaving it to be inferred. + *

+ * If your code runs on an end-user device, use the {@code io.ably.pubsub:device} artifact instead. + * + *

{@code
+ * AblyRest http = PubSubServer.httpClientBuilder()
+ *     .key("xVLyHw.MHOCLg:...")
+ *     .build();
+ *
+ * AblyRealtime realtime = PubSubServer.realtimeClientBuilder()
+ *     .key("xVLyHw.MHOCLg:...")
+ *     .echoMessages(false)
+ *     .build();
+ * }
+ */ +public final class PubSubServer { + + private PubSubServer() { + } + + /** + * Creates a builder for a stateless HTTP client, which talks to Ably over plain HTTP requests + * without holding a connection open. + *

+ * This is the right choice for most server-side work: publishing, reading history, querying + * presence, issuing tokens and push administration. + * + * @return a new builder. + */ + public static HttpClientBuilder httpClientBuilder() { + return new HttpClientBuilder(); + } + + /** + * Creates a builder for a realtime client, which holds a persistent connection to Ably and can + * subscribe to messages and presence as they happen. + *

+ * Choose this over {@link #httpClientBuilder()} only when the server needs to receive messages + * live, rather than only send them. + * + * @return a new builder. + */ + public static RealtimeClientBuilder realtimeClientBuilder() { + return new RealtimeClientBuilder(); + } + + /** + * The options common to both server-side clients. One method per {@link ClientOptions} + * property that can affect a client of either kind; {@link RealtimeClientBuilder} adds the + * properties that only mean something for a persistent connection. + * + * @param the concrete builder type, so that chaining preserves it. + */ + public abstract static class ClientBuilder> { + + /** Accumulates the calls made on this builder; handed to the client as-is by build(). */ + final ClientOptions options = new ClientOptions(); + + ClientBuilder() { + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Sets {@link Auth.AuthOptions#authCallback}. + * + * @param authCallback the value to set. + * @return this builder. + */ + public T authCallback(Auth.TokenCallback authCallback) { + options.authCallback = authCallback; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authUrl}. + * + * @param authUrl the value to set. + * @return this builder. + */ + public T authUrl(String authUrl) { + options.authUrl = authUrl; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authMethod}. + * + * @param authMethod the value to set. + * @return this builder. + */ + public T authMethod(String authMethod) { + options.authMethod = authMethod; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#key}. + * + * @param key the value to set. + * @return this builder. + */ + public T key(String key) { + options.key = key; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#token}. + * + * @param token the value to set. + * @return this builder. + */ + public T token(String token) { + options.token = token; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#tokenDetails}. + * + * @param tokenDetails the value to set. + * @return this builder. + */ + public T tokenDetails(Auth.TokenDetails tokenDetails) { + options.tokenDetails = tokenDetails; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authHeaders}. + * + * @param authHeaders the value to set. + * @return this builder. + */ + public T authHeaders(Param[] authHeaders) { + options.authHeaders = authHeaders; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authParams}. + * + * @param authParams the value to set. + * @return this builder. + */ + public T authParams(Param[] authParams) { + options.authParams = authParams; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#queryTime}. + * + * @param queryTime the value to set. + * @return this builder. + */ + public T queryTime(boolean queryTime) { + options.queryTime = queryTime; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#useTokenAuth}. + * + * @param useTokenAuth the value to set. + * @return this builder. + */ + public T useTokenAuth(boolean useTokenAuth) { + options.useTokenAuth = useTokenAuth; + return self(); + } + + /** + * Sets {@link ClientOptions#clientId}. + * + * @param clientId the value to set. + * @return this builder. + */ + public T clientId(String clientId) { + options.clientId = clientId; + return self(); + } + + /** + * Sets {@link ClientOptions#logLevel}. + * + * @param logLevel the value to set. + * @return this builder. + */ + public T logLevel(int logLevel) { + options.logLevel = logLevel; + return self(); + } + + /** + * Sets {@link ClientOptions#logHandler}. + * + * @param logHandler the value to set. + * @return this builder. + */ + public T logHandler(LogHandler logHandler) { + options.logHandler = logHandler; + return self(); + } + + /** + * Sets {@link ClientOptions#tls}. + * + * @param tls the value to set. + * @return this builder. + */ + public T tls(boolean tls) { + options.tls = tls; + return self(); + } + + /** + * Sets {@link ClientOptions#headers}. + * + * @param headers the value to set. + * @return this builder. + */ + public T headers(Map headers) { + options.headers = headers; + return self(); + } + + /** + * Sets {@link ClientOptions#restHost}. + * + * @param restHost the value to set. + * @return this builder. + */ + public T restHost(String restHost) { + options.restHost = restHost; + return self(); + } + + /** + * Sets {@link ClientOptions#port}. + * + * @param port the value to set. + * @return this builder. + */ + public T port(int port) { + options.port = port; + return self(); + } + + /** + * Sets {@link ClientOptions#tlsPort}. + * + * @param tlsPort the value to set. + * @return this builder. + */ + public T tlsPort(int tlsPort) { + options.tlsPort = tlsPort; + return self(); + } + + /** + * Sets {@link ClientOptions#useBinaryProtocol}. + * + * @param useBinaryProtocol the value to set. + * @return this builder. + */ + public T useBinaryProtocol(boolean useBinaryProtocol) { + options.useBinaryProtocol = useBinaryProtocol; + return self(); + } + + /** + * Sets {@link ClientOptions#proxy}. + * + * @param proxy the value to set. + * @return this builder. + */ + public T proxy(ProxyOptions proxy) { + options.proxy = proxy; + return self(); + } + + /** + * Sets {@link ClientOptions#environment}. + * + * @param environment the value to set. + * @return this builder. + */ + public T environment(String environment) { + options.environment = environment; + return self(); + } + + /** + * Sets {@link ClientOptions#idempotentRestPublishing}. + * + * @param idempotentRestPublishing the value to set. + * @return this builder. + */ + public T idempotentRestPublishing(boolean idempotentRestPublishing) { + options.idempotentRestPublishing = idempotentRestPublishing; + return self(); + } + + /** + * Sets {@link ClientOptions#httpOpenTimeout}. + * + * @param httpOpenTimeout the value to set. + * @return this builder. + */ + public T httpOpenTimeout(int httpOpenTimeout) { + options.httpOpenTimeout = httpOpenTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#httpRequestTimeout}. + * + * @param httpRequestTimeout the value to set. + * @return this builder. + */ + public T httpRequestTimeout(int httpRequestTimeout) { + options.httpRequestTimeout = httpRequestTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#httpMaxRetryDuration}. + * + * @param httpMaxRetryDuration the value to set. + * @return this builder. + */ + public T httpMaxRetryDuration(int httpMaxRetryDuration) { + options.httpMaxRetryDuration = httpMaxRetryDuration; + return self(); + } + + /** + * Sets {@link ClientOptions#httpMaxRetryCount}. + * + * @param httpMaxRetryCount the value to set. + * @return this builder. + */ + public T httpMaxRetryCount(int httpMaxRetryCount) { + options.httpMaxRetryCount = httpMaxRetryCount; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackHosts}. + * + * @param fallbackHosts the value to set. + * @return this builder. + */ + public T fallbackHosts(String[] fallbackHosts) { + options.fallbackHosts = fallbackHosts; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackHostsUseDefault}. + * + * @param fallbackHostsUseDefault the value to set. + * @return this builder. + * @deprecated deprecated on {@link ClientOptions} itself; use + * {@link #fallbackHosts(String[])} to supply custom hosts. + */ + @Deprecated + public T fallbackHostsUseDefault(boolean fallbackHostsUseDefault) { + options.fallbackHostsUseDefault = fallbackHostsUseDefault; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackRetryTimeout}. + * + * @param fallbackRetryTimeout the value to set. + * @return this builder. + */ + public T fallbackRetryTimeout(long fallbackRetryTimeout) { + options.fallbackRetryTimeout = fallbackRetryTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#defaultTokenParams}. + * + * @param defaultTokenParams the value to set. + * @return this builder. + */ + public T defaultTokenParams(Auth.TokenParams defaultTokenParams) { + options.defaultTokenParams = defaultTokenParams; + return self(); + } + + /** + * Sets {@link ClientOptions#asyncHttpThreadpoolSize}. + * + * @param asyncHttpThreadpoolSize the value to set. + * @return this builder. + */ + public T asyncHttpThreadpoolSize(int asyncHttpThreadpoolSize) { + options.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize; + return self(); + } + + /** + * Sets {@link ClientOptions#pushFullWait}. + * + * @param pushFullWait the value to set. + * @return this builder. + */ + public T pushFullWait(boolean pushFullWait) { + options.pushFullWait = pushFullWait; + return self(); + } + + /** + * Sets {@link ClientOptions#localStorage}. + * + * @param localStorage the value to set. + * @return this builder. + */ + public T localStorage(Storage localStorage) { + options.localStorage = localStorage; + return self(); + } + + /** + * Sets {@link ClientOptions#addRequestIds}. + * + * @param addRequestIds the value to set. + * @return this builder. + */ + public T addRequestIds(boolean addRequestIds) { + options.addRequestIds = addRequestIds; + return self(); + } + + /** + * Sets {@link ClientOptions#agents}. + * + * @param agents the value to set. + * @return this builder. + */ + public T agents(Map agents) { + options.agents = agents; + return self(); + } + } + + /** + * Builds a stateless HTTP client. Obtain one from {@link PubSubServer#httpClientBuilder()}. + *

+ * Deliberately does not expose the realtime-only options, since an {@link AblyRest} never + * opens a connection for them to apply to. + */ + public static final class HttpClientBuilder extends ClientBuilder { + + HttpClientBuilder() { + } + + /** + * Builds the client. + * + * @return an {@link AblyRest}. + * @throws AblyException if the options are invalid, for example if no authentication + * parameters were supplied. + */ + @SuppressWarnings("deprecation") // this factory is the replacement for that constructor + public AblyRest build() throws AblyException { + return new AblyRest(options); + } + } + + /** + * Builds a realtime client. Obtain one from {@link PubSubServer#realtimeClientBuilder()}. + */ + public static final class RealtimeClientBuilder extends ClientBuilder { + + RealtimeClientBuilder() { + } + + /** + * Sets {@link ClientOptions#realtimeHost}. + * + * @param realtimeHost the value to set. + * @return this builder. + */ + public RealtimeClientBuilder realtimeHost(String realtimeHost) { + options.realtimeHost = realtimeHost; + return this; + } + + /** + * Sets {@link ClientOptions#autoConnect}. + * + * @param autoConnect the value to set. + * @return this builder. + */ + public RealtimeClientBuilder autoConnect(boolean autoConnect) { + options.autoConnect = autoConnect; + return this; + } + + /** + * Sets {@link ClientOptions#queueMessages}. + * + * @param queueMessages the value to set. + * @return this builder. + */ + public RealtimeClientBuilder queueMessages(boolean queueMessages) { + options.queueMessages = queueMessages; + return this; + } + + /** + * Sets {@link ClientOptions#echoMessages}. + * + * @param echoMessages the value to set. + * @return this builder. + */ + public RealtimeClientBuilder echoMessages(boolean echoMessages) { + options.echoMessages = echoMessages; + return this; + } + + /** + * Sets {@link ClientOptions#recover}. + * + * @param recover the value to set. + * @return this builder. + */ + public RealtimeClientBuilder recover(String recover) { + options.recover = recover; + return this; + } + + /** + * Sets {@link ClientOptions#realtimeRequestTimeout}. + * + * @param realtimeRequestTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder realtimeRequestTimeout(long realtimeRequestTimeout) { + options.realtimeRequestTimeout = realtimeRequestTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#disconnectedRetryTimeout}. + * + * @param disconnectedRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder disconnectedRetryTimeout(long disconnectedRetryTimeout) { + options.disconnectedRetryTimeout = disconnectedRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#suspendedRetryTimeout}. + * + * @param suspendedRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder suspendedRetryTimeout(long suspendedRetryTimeout) { + options.suspendedRetryTimeout = suspendedRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#channelRetryTimeout}. + * + * @param channelRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder channelRetryTimeout(int channelRetryTimeout) { + options.channelRetryTimeout = channelRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#transportParams}. + * + * @param transportParams the value to set. + * @return this builder. + */ + public RealtimeClientBuilder transportParams(Param[] transportParams) { + options.transportParams = transportParams; + return this; + } + + /** + * Builds the client, which connects immediately unless {@link #autoConnect(boolean)} was + * set to false. + * + * @return an {@link AblyRealtime}. + * @throws AblyException if the options are invalid, for example if no authentication + * parameters were supplied. + */ + @SuppressWarnings("deprecation") // this factory is the replacement for that constructor + public AblyRealtime build() throws AblyException { + return new AblyRealtime(options); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dfd7150f4..24b1b2c80 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,8 @@ include("network-client-core") include("network-client-default") include("network-client-okhttp") include("pubsub-adapter") +include("pubsub-server") +include("pubsub-device") include("liveobjects") include("examples") include("uts")