<@U79JY5TL3> does the tutorial using 3.0.0-dev2 <h...
# apollo-kotlin
d
@mbonnin does the tutorial using 3.0.0-dev2 https://github.com/apollographql/apollo-android-tutorial/tree/3.0.0. use websockets for mutations and queries?
m
Hi Doug! Nice to see you here πŸ‘‹ πŸ™‚
Unfortunately not, we're missing a server to test this against
The vast majority of the servers out there use plain HTTP queries and mutations
d
I guess you don't have access to the servers used for the iOS and nodejs implementations :(
m
Maybe? Do you have a url somewhere?
d
No, I don't know what they used.
m
Yea, it's not easy to find those
d
For some reason I don't see the same Apollo client interface testing with com.apollographql.apollo3apollo coroutines support3.0.0-dev8 and com.apollographql.apollo3apollo runtime kotlin3.0.0-dev8 as I do when I cloned the repo and checked out the tag v3.0.0-dev8?
m
You can drop
apollo-coroutines-support
, Apollo 3 exposes a API interface by default
Apollo 3 is coroutines first so it has no
toFlow
to
await
d
I still don't understand why the code looks different above πŸ€”. However, removing
apollo-coroutines-support
fixed my duplicate definition issue πŸ˜€
Old code to successfully subscribe but query & mutation were POSTs..
Copy code
val okHttpClient = OkHttpClient.Builder()
        .addNetworkInterceptor(LoggingInterceptor())
        .hostnameVerifier(NullHostNameVerifier())
        .build()

wsClient = ApolloClient.builder()
        .serverUrl("https://$host/websocket/realtime/websocket")
        .subscriptionTransportFactory(WebSocketSubscriptionTransport.Factory("wss://$host/websocket/realtime/websocket", okHttpClient))
        .subscriptionConnectionParams(SubscriptionConnectionParams(mapOf("x-vcs-token" to securityToken)))
        .okHttpClient(okHttpClient)
        .build()
New code where the subscription isn't even successful (timeout) πŸ€”
Copy code
val wsFactory = ApolloWebSocketFactory("https://$host/websocket/realtime/websocket",
    mapOf("Sec-WebSocket-Protocol" to "graphql-transport-ws"))

val transport = ApolloWebSocketNetworkTransport(wsFactory, mapOf("x-vcs-token" to securityToken))

wsClient = ApolloClient.Builder()
    .subscriptionNetworkTransport(transport)
    .networkTransport(transport)
    .build()
m
Any chance you can create a small codesandbox that would use queries over websocket?
But I'm not too familiar with
graphql-ws
and other things
d
Helix looks like it doesn't use websockets. I'll have to try and see if we can't expose a very simple interface for testing. In the meantime are there apollo logs I can turn on / tools I could use? I would love to see the subscription message being sent
πŸ‘ 1
m
Your best bet is breakpoints. They usually work well
Otherwise, you could make a local build (
./gradlew publishToMavenLocal
) and point your project to
mavenLocal()
d
Our backend uses https://www.npmjs.com/package/graphql-ws it has a getting started server setup that might be useful for test purposes.
With the following code the websocket is being opened to the server but the server never receives the connection_init on time and closes the websocket with 4408: Connection initialisation timeout πŸ€”
Copy code
val wsFactory = ApolloWebSocketFactory("https://$host/websocket/realtime/websocket")

val transport = ApolloWebSocketNetworkTransport(wsFactory, mapOf("x-vcs-token" to securityToken))

wsClient = ApolloClient.Builder()
    .subscriptionNetworkTransport(transport)
    .networkTransport(transport)
    .build()
The problem is the connection_init is sent but as binary 🀨
m
Alright, finally got what I think is a test server up and running thanks to some help: https://codesandbox.io/s/6hwhx?file=/src/index.js
@Doug Chappelle any chance you can try against this? See if it reproduces your issue?
d
@mbonnin I'll take a look. I've gotten further by sending text instead of bytes in the websocket. The channel's still use ByteString but I'm not sure that matters. I'm to the point that can successfully subscribe but query and mutate are crashing on a casting issue e.g.,
Failed to retrieve TURN info: java.lang.ClassCastException: com.unify.vcs.realtime-svc-api.TurnInfoByIdQuery cannot be cast to com.apollographql.apollo3.api.Subscription
m
Is there more to this stacktrace? Looks like there's some place where the client harcodes subscriptions...
d
backtrace = {Object[41]@18974} 0 = {int[80]@18982} 1 = {Class@18265} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$execute$$inlined$flatMapLatest$1$lambda$4" 2 = {Class@18265} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$execute$$inlined$flatMapLatest$1$lambda$4" 3 = {Class@18355} "class kotlinx.coroutines.flow.FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1" 4 = {Class@18353} "class kotlinx.coroutines.flow.FlowKt__EmittersKt$onCompletion$$inlined$unsafeFlow$1" 5 = {Class@18025} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$execute$$inlined$flatMapLatest$1" 6 = {Class@18025} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$execute$$inlined$flatMapLatest$1" 7 = {Class@18235} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1$lambda$1" 8 = {Class@18235} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1$lambda$1" 9 = {Class@17320} "class kotlinx.coroutines.intrinsics.UndispatchedKt" 10 = {Class@16834} "class kotlinx.coroutines.CoroutineStart" 11 = {Class@17317} "class kotlinx.coroutines.AbstractCoroutine" 12 = {Class@17450} "class kotlinx.coroutines.BuildersKt__Builders_commonKt" 13 = {Class@16832} "class kotlinx.coroutines.BuildersKt" 14 = {Class@17450} "class kotlinx.coroutines.BuildersKt__Builders_commonKt" 15 = {Class@16832} "class kotlinx.coroutines.BuildersKt" 16 = {Class@18229} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1" 17 = {Class@18261} "class kotlinx.coroutines.flow.FlowKt__TransformKt$filterNotNull$$inlined$unsafeTransform$1$2" 18 = {Class@18247} "class kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1" 19 = {Class@18247} "class kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1" 20 = {Class@18126} "class kotlinx.coroutines.flow.internal.SafeCollector" 21 = {Class@18126} "class kotlinx.coroutines.flow.internal.SafeCollector" 22 = {Class@18012} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$getServerConnection$1" 23 = {Class@18012} "class com.apollographql.apollo3.network.ws.ApolloWebSocketNetworkTransport$getServerConnection$1" 24 = {Class@18121} "class kotlinx.coroutines.flow.SafeFlow" 25 = {Class@18120} "class kotlinx.coroutines.flow.AbstractFlow" 26 = {Class@18254} "class kotlinx.coroutines.flow.FlowKt__TransformKt$filterNotNull$$inlined$unsafeTransform$1" 27 = {Class@18145} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3" 28 = {Class@18145} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3" 29 = {Class@17320} "class kotlinx.coroutines.intrinsics.UndispatchedKt" 30 = {Class@18146} "class kotlinx.coroutines.flow.internal.FlowCoroutineKt" 31 = {Class@18135} "class kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest" 32 = {Class@18134} "class kotlinx.coroutines.flow.internal.ChannelFlowOperator" 33 = {Class@18134} "class kotlinx.coroutines.flow.internal.ChannelFlowOperator" 34 = {Class@18139} "class kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1" 35 = {Class@16827} "class kotlin.coroutines.jvm.internal.BaseContinuationImpl" 36 = {Class@16732} "class kotlinx.coroutines.DispatchedTask" 37 = {Class@17392} "class kotlinx.coroutines.scheduling.CoroutineScheduler" 38 = {Class@17407} "class kotlinx.coroutines.scheduling.CoroutineScheduler$Worker" 39 = {Class@17407} "class kotlinx.coroutines.scheduling.CoroutineScheduler$Worker" 40 = {Class@17407} "class kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
I can look into it, give me a bit of time as I'm wrapping up something, will look into it this week
πŸ™Œ 1
d
I hacked the code to use subscription: Operation<*> instead. I can now send query & mutations over the websocket but I'm working on a new issue now... not sure if my hacks to apollo are involved
m
Been digging a bit more but I can't get the
graphql-ws
samples to work. I managed to get to the point where both client and server start but don't talk to each other
Websockets should work over localhost, right?
Alright, it was because of
wss
instead of
ws
πŸ€¦β€β™‚οΈ
d
I have an updated/hacked version with graphql-ws working and queries & subscribes using the websocket. For the websocket part I only had to make the change I mentioned above to fix the cast and send data as a string and not bytes πŸ™‚. I can share the changes with you if you like.
m
Oh yes please πŸ™ That'd be awesome
d
https://github.com/DNogginator-Inc/apollo-android/tree/graphql-ws Branched off of v3.0.0-dev8. Ignore changes in-between 'changes to use graphql-ws' and 'Fix tests'... I was trying to hack out some of the testing to cut down on JitPack loading time but fixing the tests for my graphql-ws purposes actually exposed something I missed :/
❀️ 1
πŸ™ 1
m
Ah pretty cool!!
I'm trying to get that in a state where it can co-exists with the existing protocol
d
yeah.. I wasn't worried about that too much 😬
m
Slightly confused by
Copy code
-      webSocket.send(data)
        +      webSocket.send(String(data.toByteArray()))
Isn't that the same thing ultimately?
Like I'd expect OkHttp to encode the string to bytes before sending them to the wire?
Oooohh forget what I said, I just realized that WebSockets have type 1 and type 2 frames πŸ‘
Interesting
d
πŸ‘
m
That took a while but I finally landed something that I think would work with both protocols: https://github.com/apollographql/apollo-android/pull/3102/files#diff-8052d8e0b178f17902b10de1a636160ef67d4ae3ff96d64bed63b92a600a9641
Strangely, I didn't need to send the data as text, sending binary worked so there might be something to investigate there
d
I didn't need to send the data as text, sending binary worked
I think it's just our server is expecting text not binary. Would there be a way to configure this? I could give it a try on our backend.
m
Gotcha. What is your server based on out of curiosity?
I'll make that configurable
d
Our server is a Java / Spring backend and actually implements the Graphql over WebSocket using graphql-ws protocol itself. Once configurable let me know and I'll give it a test drive πŸ™‚
πŸ‘ 1
m
3.0.0-dev10
is out with configurable websocket protocol.
It defaults to
WsFrameType.Binary
. To change to text, use
Copy code
GraphQLWsProtocol(frameType = WsFrameType.Text)
d
Awesome, I'll give it a try asap and let you know
πŸ‘ 1
The previous ApolloWebSocketNetworkTransport constructor had a parameter connectionParams. I need this functionality... is there a way to do it with 3.0.0-dev10 ?
m
I think it's still there for the
SubscriptionWsProtocol
but might have gone in
GraphqlWsProtocol
it should be relatively easy to add it, feel free to send a PR. I'm off today and tomorrow but can look into this on Monday
Worst case, you should be able to implement your own
WsProtocol
I'm hoping this API can accommodate more custom use case like graphql-ws and maybe appsync
d
It was very simple to add the connectionPayload in the GraphQLWsProtocol, thanks!
πŸ‘ 1
m
Made a PR there: https://github.com/apollographql/apollo-android/pull/3110. Just curious, what is it used for? Authentication I guess?
d
Correct, authentication purposes, thanks for the update!
πŸ‘ 1
Hi @mbonnin, simple question... there is no 'unsubscribe' with the ApolloClient that I see.. can you tell me the proper way this should be done?
m
It's all done using the coroutine APIs. If you want to cancel a request, you can wrap it in launch {}Β and cancel the job
Copy code
job = launch {
  val response = apolloClient.query(request)
  // do something with response
}

// Cancel here if needed
job.cancel()
Most of the times, you should have a scope that will cancel the coroutine for you. For an example, on Android, the lifecycleScope should cancel the coroutine if you leave the Activity
πŸ‘ 1
d
Hi @mbonnin, we have cases in which the socket will be closed e.g., the connection token is expired. In that case the Websocket shutdown code 4401: Unauthorized is sent by the backend. Is there a way to get those instead of the ApolloNetworkException (ACK not received after x amount of time)?
m
Mmmm I would expect all the currently open subscription to receive an error. The
"ACK not received after x amount of time"
should come from another attempt to read the WebSocket. Maybe a
retry
?
d
I receive the "ACK not received after x amount of time" in the subscription.retryWhen block. However, I don't see the original error πŸ€”
m
Any chance you can reproduce with https://github.com/martinbonnin/graphql-ws-server ?
d
I tried using your server. I don't see a connection error when I subscribe, I just see the "ACK not received..." message
m
So no message received at all?
d
just the ACK message
m
Mmmm that's unexpected
I'll double check this
πŸ‘ 1
Alright, I don't know how to write typescript apparently 😬 https://github.com/martinbonnin/graphql-ws-server/commit/f93c60f4a77ac42310abf42f9dca675fd05ac756
If you could modify
graphql-ws-server
to exhibit the error, that'd be super helpful
d
I'm afraid I don't know typescript at all 😬 but the test case would be to close the socket with a 4401 and see what your test client receives
m
πŸ‘. Do you mind opening an issue for this? I can look into this but that'll take a bit of time so having an issue will make sure I don't forget
d
Hi @mbonnin, i'm trying to build v3.0.0-alpha01 but get unresolved reference build errors for CacheQueries & ApolloDatabase?
m
That's a sqldelight issue
Remove your build folders and it should go away
d
I tried that but couldn't get it to work... removed and recloned the repo and it works now (I thought I removed all the build folders πŸ€”) . Now the plugin is complaining about my schema file... directive 'include' is defined multiple times but it is not. I read the migration guide but didn't see anything about this problem?
m
Do you mind sharing your schema?
d
Copy code
"Directs the executor to include this field or fragment only when the `if` argument is true"
directive @include(
    "Included when true."
    if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

"Directs the executor to skip this field or fragment when the `if`'argument is true."
directive @skip(
    "Skipped when true."
    if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
. . .
The schema is the same as I was using with v3.0.0-dev10
m
I see. Since these are builtin we add them too. Let me see if there's something in the spec
When representing a GraphQL schema using the type system definition language anyΒ built-in directiveΒ may be omitted for brevity.
That's a may, not a must so we should be robust to that. I'll make a fix
In between, you can safely remove them
πŸ‘ 1
d
works great!
πŸ‘ 1
m
Also. I love this thread but feel free to open new ones for new errors/topics. This way, other people in the channel get a chance to chip in too
d
I'll start using a new thread πŸ™‚ but the unresolved reference build errors for CacheQueries & ApolloDatabase happens after a gradlew clean is done... and I went through to check all build folders are gone 😞
m
Usually it's the
apollo-normalized-cache-sqlite
. Doing
Copy code
rm -rf apollo-normalized-cache-sqlite/build
usually fixes it