|
| 1 | +--- |
| 2 | +title: Experimental websockets |
| 3 | +--- |
| 4 | + |
| 5 | +> ⚠️ **Experimental websockets APIs are [experimental](https://www.apollographql.com/docs/resources/release-stages/#experimental-features) in Apollo Kotlin.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-kotlin/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Defer%20Support]) or in the [Kotlin Slack community](https://slack.kotl.in/). |
| 6 | +
|
| 7 | + |
| 8 | +Historically, WebSockets have been one of the most complex and error-prone parts of Apollo Kotlin because: |
| 9 | + |
| 10 | +1. The WebSocket transport protocol has no official specification and different implementations have different behaviours. |
| 11 | +2. WebSockets are stateful and making them work using the old Kotlin native memory model was challenging. |
| 12 | +3. Because WebSockets are long-lived connections, they are more exposed to errors and knowing when (or if) to retry is hard. |
| 13 | +4. Not all subscriptions happen on WebSockets. Some use [HTTP multipart](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/) for an example. |
| 14 | + |
| 15 | +Starting with 4.0.0, Apollo Kotlin provides a new `com.apollographql.apollo3.network.websocket` package containing new `WebSocketNetworkTransport` and `WebSocketEngine` implementations (instead of `com.apollographql.apollo3.network.ws` for the current implementations). |
| 16 | + |
| 17 | +The `com.apollographql.apollo3.network.websocket` implementation provides the following: |
| 18 | + |
| 19 | +1. Defaults to the [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol, which has become the de facto standard. Using the other protocols is still possible but having a main, specified, protocol ensures we can write a good and solid test suite. |
| 20 | +2. Does not inherit from the old memory model design, making the code considerably simpler. In particular, `WebSocketEngine` is now event based and no attempt at flow control is done. If you buffers grow too much, your subscription fails. |
| 21 | +3. Plays nicely with the ApolloClient `retryOnError` API. |
| 22 | +4. Handles different Subscription transports more consistently. |
| 23 | + |
| 24 | +## Status |
| 25 | + |
| 26 | +While they are `@ApolloExperimental`, we believe the new `.websocket` APIS to be more robust than the non-experimental `.ws` ones. They are safe to use in non-lib use cases. |
| 27 | + |
| 28 | +The "experimental" tag is to account for required API breaking changes based on community feedback. Ideally no change is needed. |
| 29 | + |
| 30 | +After a feedback phase, the current `.ws` APIs will become deprecated and the `.websocket` one promoted to stable by removing the `@ApolloExperimental` annotations. |
| 31 | + |
| 32 | +## Migration guide |
| 33 | + |
| 34 | +In simple cases where you did not configure the underlying `WsProtocol` or retry logic, the migration should be about replacing `com.apollographql.apollo3.network.ws` with `com.apollographql.apollo3.network.websocket` everywhere: |
| 35 | + |
| 36 | +```kotlin |
| 37 | +// Replace |
| 38 | +import com.apollographql.apollo3.network.ws.WebSocketNetworkTransport |
| 39 | +import com.apollographql.apollo3.network.ws.WebSocketEngine |
| 40 | +// etc... |
| 41 | + |
| 42 | +// With |
| 43 | +import com.apollographql.apollo3.network.websocket.WebSocketNetworkTransport |
| 44 | +import com.apollographql.apollo3.network.websocket.WebSocketEngine |
| 45 | +// etc... |
| 46 | +``` |
| 47 | + |
| 48 | +Because we can't remove the current APIs just yet, the `ApolloClient.Builder` shortcut APIs are still pointing to the `.ws` implementations. To use the newer `.websocket` implementation, pass a `websocket.WebSocketNetworkTransport` directly: |
| 49 | + |
| 50 | +```kotlin |
| 51 | +// Replace |
| 52 | +val apolloClient = ApolloClient.Builder() |
| 53 | + .serverUrl(serverUrl) |
| 54 | + .webSocketServerUrl(webSocketServerUrl) |
| 55 | + .webSocketEngine(myWebSocketEngine) |
| 56 | + .webSocketIdleTimeoutMillis(10_000) |
| 57 | + .build() |
| 58 | + |
| 59 | +// With |
| 60 | +import com.apollographql.apollo3.network.websocket.* |
| 61 | + |
| 62 | +// [...] |
| 63 | + |
| 64 | +ApolloClient.Builder() |
| 65 | + .serverUrl(serverUrl) |
| 66 | + .subscriptionNetworkTransport( |
| 67 | + WebSocketNetworkTransport.Builder() |
| 68 | + .serverUrl(webSocketServerUrl) |
| 69 | + // If you didn't set a WsProtocol before, make sure to include this |
| 70 | + .wsProtocol(SubscriptionWsProtocol()) |
| 71 | + // If you were already using GraphQLWsProtocol, this is now the default |
| 72 | + //.wsProtocol(GraphQLWsProtocol()) |
| 73 | + .webSocketEngine(myWebSocketEngine) |
| 74 | + .idleTimeoutMillis(10_000) |
| 75 | + .build() |
| 76 | + ) |
| 77 | + .build() |
| 78 | +``` |
| 79 | + |
| 80 | +To account for non-websocket transports, like [multipart subscriptions](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/), the retry is now handled on the `ApolloClient` instead of the `NetworkTransport`. |
| 81 | + |
| 82 | +```kotlin |
| 83 | +// Replace |
| 84 | +val apolloClient = ApolloClient.Builder() |
| 85 | + .subscriptionNetworkTransport( |
| 86 | + WebSocketNetworkTransport.Builder() |
| 87 | + .serverUrl(url) |
| 88 | + .reopenWhen { _, _ -> |
| 89 | + delay(1000) |
| 90 | + true |
| 91 | + } |
| 92 | + .build() |
| 93 | + ) |
| 94 | + |
| 95 | +// With |
| 96 | +val apolloClient = ApolloClient.Builder() |
| 97 | + .subscriptionNetworkTransport( |
| 98 | + WebSocketNetworkTransport.Builder() |
| 99 | + .serverUrl(url) |
| 100 | + .build() |
| 101 | + ) |
| 102 | + // Only retry subscriptions |
| 103 | + .retryOnError { it.operation is Subscription } |
| 104 | +``` |
| 105 | + |
| 106 | +The above uses the default retry algorithm: |
| 107 | + |
| 108 | +* Wait until the network is available if you configured a [NetworkMonitor](network-connectivity). |
| 109 | +* Or use exponential backoff else. |
| 110 | + |
| 111 | +To customize the retry logic more, use `addRetryOnErrorInterceptor`: |
| 112 | + |
| 113 | +```kotlin |
| 114 | +val apolloClient = ApolloClient.Builder() |
| 115 | + .subscriptionNetworkTransport( |
| 116 | + WebSocketNetworkTransport.Builder() |
| 117 | + .serverUrl(url) |
| 118 | + .build() |
| 119 | + ) |
| 120 | + .addRetryOnErrorInterceptor { apolloRequest, exception, attempt -> |
| 121 | + // retry logic here |
| 122 | + |
| 123 | + // return true to retry or false to terminate the Flow |
| 124 | + true |
| 125 | + } |
| 126 | +``` |
0 commit comments