kabiroberai / node-swift

Create Node modules in Swift
MIT License
416 stars 11 forks source link

Inconsistent behavior with using DispatchQueue on Node vs Electron #4

Open KishanBagaria opened 2 years ago

KishanBagaria commented 2 years ago
import NodeAPI
import Foundation

final class AwesomeClass: NodeClass {
  static let properties: NodeClassPropertyList = [
    "foo": NodeMethod(foo),
  ]
  private static let queue = DispatchQueue(label: "new-queue")
  private let swiftJSQueue: NodeAsyncQueue

  init(_ args: NodeFunction.Arguments) throws {
    self.swiftJSQueue = try NodeAsyncQueue(label: "new-async")
  }

  private static func returnAsync(
      on jsQueue: NodeAsyncQueue,
      _ action: @escaping () throws -> NodeValueConvertible
  ) throws -> NodePromise {
      try NodePromise { deferred in
          queue.async {
              let result = Result { try action() }
              try? jsQueue.async {
                  try deferred(result)
              }
          }
      }
  }
  private func returnAsync(
      _ action: @escaping () throws -> NodeValueConvertible
  ) throws -> NodePromise {
      try Self.returnAsync(on: swiftJSQueue, action)
  }
  private func performAsync(
      _ action: @escaping () throws -> Void
  ) throws -> NodePromise {
      try returnAsync {
          try action()
          return NodeUndefined.deferred
      }
  }

  func foo(bar: String, baz: String) throws -> NodeValueConvertible {
    try performAsync { 
      print("\(bar) \(baz)")
      DispatchQueue.main.sync {
        print("inside queue: \(bar) \(baz)")
      }
      print("outside queue: \(bar) \(baz)")
    }
  }
}

@main struct AwesomeMod: NodeModule {
  let exports: NodeValueConvertible

  init() throws {
    exports = try NodeObject([
      "AwesomeClass": AwesomeClass.constructor(),
    ])
  }
}
const swift = require(`./binaries/${process.platform}-${process.arch}/node.node`)

new swift.AwesomeClass().foo("a", "b").then(() => console.log("foo promise resolved"))
console.log('done')
$ node index.js
a b
done
^C

$ electron index.js
a b
done
inside queue: a b
outside queue: a b
foo promise resolved
^Cnode_modules/electron/dist/Electron.app/Contents/MacOS/Electron exited with signal SIGINT
kabiroberai commented 2 years ago

Reduced example:

import NodeAPI
import Foundation

@main struct AwesomeMod: NodeModule {
  let exports: NodeValueConvertible
  init() throws {
    exports = try NodeFunction { _ in
        DispatchQueue(label: "new-queue").async {
          print("before queue")
          DispatchQueue.main.sync {
            print("inside queue")
          }
          print("after queue")
        }
        // RunLoop.main.run()
        return NodeUndefined.deferred
      }
  }
}

This is happening because DispatchQueue.main.sync enqueues the block onto the main NSRunLoop. Vanilla Node doesn't have a running NSRunLoop, whereas Electron is built to explicitly handle this case (see https://youtu.be/OPhb5GoV8Xk). You can sort of work around this by starting the RunLoop at the end of the function, though that causes it to never return. We should look into servicing the main run loop in a vanilla Node environment alongside libuv, as Electron does.