Proper way to call back into JavaScriptCore from native

In our tvOS app we have a couple of hooks into the native side where we receive events over a WebSocket in (using CocoaAsyncSocket) native and propagate those events into the JavaScript layer. The calls into the JS layer are structured like this:

self.receiveQueue.addOperation {
    websocket.invokeMethod("onmessage", withArguments: [["data": text]])
}


Where `receiveQueue` is an `OperationQueue`. This works great most of the time, but we occasionally see application crashes where it seems a memory reference has gone bad. However, we've found that if we take the JavaScript function being called and wrap it in a


setTimeout(() => {
  // Do the work here
}, 0)


this resolves nearly all the issues. We still get an application crash here and there but the `setTimeout` does the trick most of the time.


This leads me to the following question. What's the proper way to call back to a TVJS application from native? Should I be using DispatchQueue.main.async { } instead of an OperationQueue? How does this relate to doing work on the main UI thread?


Thanks!

Replies

Always always use evaluateInJavaScriptContext: from TVApplicationController.

Can you provide an example? Or possibly show how I'd pass that reference from the TVApplicationController to a class that wants to use it?


I'm retrying to create separate manageable components and not have this giant TVApplicationController class.


Also the other challenge I have is the flow of information starts in JS by calling native to create an instance of a class. Then native uses that reference it gets from JS to call back to the JS layer.


Thanks!

Here's a stripped down example of the flow of code:

  1. The application adds 'webSocketManager' to the JS context.
  2. The JS layer creates a new instance of WebSocket, calling the webSocketManager.openWebSocket() method.
  3. The Websocket Manager saves a reference to the object that was passed to it, and invokes the `onmessage` handler when it receives messages.


It's not entirely clear to me how I'd use the evaluteInJavaScriptContext to meet this need.


// App Controller
func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) {
  let webSocketManager : WebSocketManager = WebSocketManager()
  jsContext.setObject(webSocketManager, forKeyedSubscript: "webSocketManager" as (NSCopying & NSObjectProtocol)!)
}

// Websocket Manager
@objc protocol WebSocketManagerExport : JSExport {
  func webSocketOpen(_ websocket: JSValue, _ url: String)
}

@objc open class WebSocketManager: NSObject, WebSocketManagerExport {
  fileprivate var webSocket: WebSocket!
  let receiveQueue = OperationQueue()

  func webSocketOpen(_ websocket: JSValue, _ url: String) {
       self.webSocket = WebSocket(url)
       self.webSocket.event.open = {
            self.receiveQueue.addOperation {
                 websocket.invokeMethod("onopen", withArguments: [])
            }
       }

       self.webSocket.event.message = { message in
            if let text = message as? String {
                 self.receiveQueue.addOperation {
                      websocket.invokeMethod("onmessage", withArguments: [["data": text]])
                 }
            }
       }
  }
}

// JS WebSocket
class WebSocket {
  constructor(url, protocols) {
    this.url = url;
    this.protocols = protocols;

    this.openWebSocket();
  }

  openWebSocket() {
    if (webSocketManager != null) {
      webSocketManager.webSocketOpen(this, this.url);
    }
  }

  set onmessage(onmessage) {
    this._onmessage = function() {
      setTimeout(() => {
        onmessage.apply(this, arguments);
      }, 0);
    };
  }
}

Hi ,

Did you able to achieve this ? I am trying to do the same thing. I want to use the websocket in js runtime in IOS.

When I am directly using "ws" npm module and create a bundle of it , it is saying "websocket" is not a constructor. So i think i have to used the native websockets only with similar approach you have used.

Can you share did you find the answer for the same or do you have another solution for this ?