All Downloads are FREE. Search and download functionalities are using the official Maven repository.

appleMain.com.apollographql.apollo.network.websocket.WebSocketEngine.apple.kt Maven / Gradle / Ivy

package com.apollographql.apollo.network.websocket

import com.apollographql.apollo.api.http.HttpHeader
import com.apollographql.apollo.exception.DefaultApolloException
import com.apollographql.apollo.network.toNSData
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlinx.cinterop.convert
import okio.ByteString.Companion.toByteString
import platform.Foundation.NSData
import platform.Foundation.NSMutableURLRequest
import platform.Foundation.NSOperationQueue
import platform.Foundation.NSURL
import platform.Foundation.NSURLSession
import platform.Foundation.NSURLSessionConfiguration
import platform.Foundation.NSURLSessionWebSocketCloseCode
import platform.Foundation.NSURLSessionWebSocketDelegateProtocol
import platform.Foundation.NSURLSessionWebSocketMessage
import platform.Foundation.NSURLSessionWebSocketMessageTypeData
import platform.Foundation.NSURLSessionWebSocketMessageTypeString
import platform.Foundation.NSURLSessionWebSocketTask
import platform.Foundation.setHTTPMethod
import platform.Foundation.setValue
import platform.darwin.NSObject

internal class AppleWebSocketEngine : WebSocketEngine {
  private val delegate = Delegate()
  private val nsUrlSession = NSURLSession.sessionWithConfiguration(
      configuration = NSURLSessionConfiguration.defaultSessionConfiguration,
      delegate = delegate,
      delegateQueue = NSOperationQueue()
  )

  override fun newWebSocket(url: String, headers: List, listener: WebSocketListener): WebSocket {
    val serverUrl = NSURL(string = url)

    val request = NSMutableURLRequest.requestWithURL(serverUrl).apply {
      headers.forEach { setValue(it.value, forHTTPHeaderField = it.name) }
      setHTTPMethod("GET")
    }

    val task = nsUrlSession.webSocketTaskWithRequest(request)
    val webSocket = AppleWebSocket(task, listener)
    delegate.associate(task, webSocket)
    webSocket.connect()

    return webSocket
  }

  override fun close() {
    delegate.close()
    nsUrlSession.invalidateAndCancel()
  }
}


private class Delegate: NSObject(), NSURLSessionWebSocketDelegateProtocol {
  private val lock = reentrantLock()
  private val map = mutableMapOf()

  fun associate(webSocketTask: NSURLSessionWebSocketTask, webSocket: AppleWebSocket) {
    lock.withLock {
      map.put(webSocketTask, webSocket)
    }
  }

  override fun URLSession(session: NSURLSession, webSocketTask: NSURLSessionWebSocketTask, didOpenWithProtocol: String?) {
    val webSocket = lock.withLock {
      map.get(webSocketTask)
    }
    webSocket?.onOpen()
  }

  override fun URLSession(
      session: NSURLSession,
      webSocketTask: NSURLSessionWebSocketTask,
      didCloseWithCode: NSURLSessionWebSocketCloseCode,
      reason: NSData?,
  ) {
    val webSocket = lock.withLock {
      val ws = map.get(webSocketTask)

      map.remove(webSocketTask)
      ws
    }
    webSocket?.onClosed(didCloseWithCode.convert(), reason?.toByteString()?.utf8())
  }

  fun close() {
    lock.withLock {
      map.clear()
    }
  }
}

/**
 * Peculiarities of NSURLSesssionWebSocketTask:
 * - cancelWithCloseCode(code) calls didCloseWithCode with the same client code, making it impossible to detect the server close code
 * - sometimes cancelWithCloseCode(code) doesn't send the close frame to the server (https://developer.apple.com/forums/thread/679446)
 * - when the server close frame is received, the received completion handler is called first with an error, making it quite difficult
 * to detect server close
 */
internal class AppleWebSocket(
    private val nsurlSessionWebSocketTask: NSURLSessionWebSocketTask,
    private val listener: WebSocketListener,
) : WebSocket {
  private val disposed = atomic(false)

  internal fun connect() {
    nsurlSessionWebSocketTask.resume()
    receiveNext()
  }

  fun onOpen() {
    listener.onOpen()
  }

  fun onClosed(code: Int?, reason: String?) {
    if (disposed.compareAndSet(expect = false, update = true)) {
      listener.onClosed(code, reason)
    }
  }

  private fun receiveNext() {
    nsurlSessionWebSocketTask.receiveMessageWithCompletionHandler { message, nsError ->
      if (nsError != null) {
        if (disposed.compareAndSet(expect = false, update = true)) {
          listener.onError(DefaultApolloException("Error reading websocket: ${nsError.localizedDescription}"))
        }
      } else if (message != null) {
        when (message.type) {
          NSURLSessionWebSocketMessageTypeData -> {
            listener.onMessage(message.data!!.toByteString().toByteArray())
          }

          NSURLSessionWebSocketMessageTypeString -> {
            listener.onMessage(message.string!!)
          }
        }

        receiveNext()
      }
    }
  }

  override fun send(data: ByteArray) {
    nsurlSessionWebSocketTask.sendMessage(NSURLSessionWebSocketMessage(data = data.toNSData())) {}
  }

  override fun send(text: String) {
    nsurlSessionWebSocketTask.sendMessage(NSURLSessionWebSocketMessage(string = text)) {}
  }

  override fun close(code: Int, reason: String) {
    if (disposed.compareAndSet(expect = false, update = true)) {
      nsurlSessionWebSocketTask.cancelWithCloseCode(code.convert(), reason.encodeToByteArray().toNSData())
    }
  }
}

actual fun WebSocketEngine(): WebSocketEngine = AppleWebSocketEngine()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy