Skip to content

Commit

Permalink
Merge pull request #488 from player-ui/jvm-async-node-ability-to-remo…
Browse files Browse the repository at this point in the history
…ve-resolved-async-node

jvm and ios-async-node-ability-to-remove-resolved-async-node
  • Loading branch information
sakuntala-motukuri authored Sep 3, 2024
2 parents ccc758a + 32b9c57 commit 71c18c7
Show file tree
Hide file tree
Showing 9 changed files with 659 additions and 173 deletions.
97 changes: 71 additions & 26 deletions ios/core/Sources/Types/Hooks/Hook.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Hook.swift
//
//
//
// Created by Borawski, Harris on 2/12/20.
//
Expand Down Expand Up @@ -41,7 +41,7 @@ public class Hook<T>: BaseJSHook where T: CreatedFromJSValue {
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping (T) -> Void) {
let tapMethod: @convention(block) (JSValue?) -> Void = { value in
Expand All @@ -66,7 +66,7 @@ public class Hook2<T, U>: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping (T, U) -> Void) {
let tapMethod: @convention(block) (JSValue?, JSValue?) -> Void = { value, value2 in
Expand All @@ -93,7 +93,7 @@ public class HookDecode<T>: BaseJSHook where T: Decodable {
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping (T) -> Void) {
let tapMethod: @convention(block) (JSValue?) -> Void = { value in
Expand All @@ -118,7 +118,7 @@ public class Hook2Decode<T, U>: BaseJSHook where T: Decodable, U: Decodable {
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping (T, U) -> Void) {
let tapMethod: @convention(block) (JSValue?, JSValue?) -> Void = { value, value2 in
Expand Down Expand Up @@ -151,28 +151,73 @@ public class AsyncHook<T>: BaseJSHook where T: CreatedFromJSValue {
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping AsyncHookHandler) {
let tapMethod: @convention(block) (JSValue?) -> JSValue = { value in
guard
let val = value,
let hookValue = T.createInstance(value: val) as? T
else { return JSValue() }

let promise =
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
Task {
let result = try await hook(hookValue)
DispatchQueue.main.async {
resolve(result as Any)
}
}
})

return promise ?? JSValue()
}

self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
}
guard
let val = value,
let hookValue = T.createInstance(value: val) as? T
else { return JSValue() }

let promise =
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
Task {
let result = try await hook(hookValue)
DispatchQueue.main.async {
resolve(result as Any)
}
}
})

return promise ?? JSValue()
}

self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
}
}

/**
This class represents an object in the JS runtime that can be tapped into
to receive JS events that has 2 parameters and
returns a promise that resolves when the asynchronous task is completed
*/
public class AsyncHook2<T, U>: BaseJSHook where T: CreatedFromJSValue, U: CreatedFromJSValue {
private var handler: AsyncHookHandler?

public typealias AsyncHookHandler = (T, U) async throws -> JSValue?

/**
Attach a closure to the hook, so when the hook is fired in the JS runtime
we receive the event in the native runtime

- parameters:
- hook: A function to run when the JS hook is fired
*/
public func tap(_ hook: @escaping AsyncHookHandler) {
let tapMethod: @convention(block) (JSValue?,JSValue?) -> JSValue = { value, value2 in
guard
let val = value,
let val2 = value2,
let hookValue = T.createInstance(value: val) as? T,
let hookValue2 = U.createInstance(value: val2) as? U
else { return JSValue() }


let promise =
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
Task {
let result = try await hook(hookValue, hookValue2)
DispatchQueue.main.async {
resolve(result as Any)
}
}
})

return promise ?? JSValue()
}

self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public val Runtime<*>.Promise: Promise.Api get() = getObject("Promise")?.let { p
} ?: throw PlayerRuntimeException("'Promise' not defined in runtime")

/** Helper to bridge complex [Promise] logic with the JS promise constructor */
public fun <T : Any> Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise {
public fun <T : Any?> Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise {
val key = "promiseHandler_${UUID.randomUUID().toString().replace("-", "")}"
add(key) { resolve: Invokable<Any?>, reject: Invokable<Any?> ->
runtime.scope.launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.intuit.playerui.core.bridge.hooks

import com.intuit.hooks.AsyncParallelBailHook
import com.intuit.hooks.BailResult
import com.intuit.hooks.HookContext
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable

@OptIn(ExperimentalCoroutinesApi::class)
@Serializable(with = NodeAsyncParallelBailHook2.Serializer::class)
public class NodeAsyncParallelBailHook2<T1, T2, R : Any?>(
override val node: Node,
serializer1: KSerializer<T1>,
serializer2: KSerializer<T2>,
) : AsyncParallelBailHook<suspend (HookContext, T1, T2) -> BailResult<R>, R>(), AsyncNodeHook<R> {

init {
init(serializer1, serializer2)
}

override suspend fun callAsync(context: HookContext, serializedArgs: Array<Any?>): R {
require(serializedArgs.size == 2) { "Expected exactly two arguments, but got ${serializedArgs.size}" }
val (p1, p2) = serializedArgs
val result = call(10) { f, _ ->
f(context, p1 as T1, p2 as T2)
} as R
return result
}

internal class Serializer<T1, T2, R : Any>(
private val serializer1: KSerializer<T1>,
private val serializer2: KSerializer<T2>,
`_`: KSerializer<R>,
) : NodeWrapperSerializer<NodeAsyncParallelBailHook2<T1, T2, R>>({
NodeAsyncParallelBailHook2(it, serializer1, serializer2)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal interface NodeHook<R> : NodeWrapper {
fun call(context: HookContext, serializedArgs: Array<Any?>): R
}

internal interface AsyncNodeHook<R : Any> : NodeHook<Promise> {
internal interface AsyncNodeHook<R : Any?> : NodeHook<Promise> {
override fun call(context: HookContext, serializedArgs: Array<Any?>): Promise = node.runtime.Promise { resolve, reject ->
val result = callAsync(context, serializedArgs)
resolve(result)
Expand Down
125 changes: 66 additions & 59 deletions plugins/async-node/ios/Sources/AsyncNodePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,59 @@

import Foundation
import JavaScriptCore

#if SWIFT_PACKAGE
import PlayerUI
#endif

public typealias AsyncHookHandler = (JSValue) async throws -> AsyncNodeHandlerType
public typealias AsyncHookHandler = (JSValue, JSValue) async throws -> AsyncNodeHandlerType

public enum AsyncNodeHandlerType {
case multiNode([ReplacementNode])
case singleNode(ReplacementNode)
case emptyNode
}

/// Extension for `ReplacementNode` to convert it to a `JSValue` in a given `JSContext`.
public extension ReplacementNode {
/// Converts the `ReplacementNode` to a `JSValue` in the provided `JSContext`.
///
/// - Parameter context: The `JSContext` in which the `JSValue` will be created.
/// - Returns: A `JSValue` representing the `ReplacementNode`, or `nil` if the conversion fails.
func toJSValue(context: JSContext) -> JSValue? {
switch self {
case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
return nil
}
case .concrete(let jsValue):
return jsValue
}
}
}

/// Extension for `AsyncNodeHandlerType` to convert it to a `JSValue` in a given `JSContext`.
public extension AsyncNodeHandlerType {
/// Converts the `AsyncNodeHandlerType` to a `JSValue` in the provided `JSContext`.
///
/// - Parameter context: The `JSContext` in which the `JSValue` will be created.
/// - Returns: A `JSValue` representing the `AsyncNodeHandlerType`, or `nil` if the conversion fails.
func handlerTypeToJSValue(context: JSContext) -> JSValue? {
switch self {
case .multiNode(let replacementNodes):
let jsValueArray = replacementNodes.compactMap {
$0.toJSValue(context: context)
}
return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])

case .singleNode(let replacementNode):
return replacementNode.toJSValue(context: context)

case .emptyNode:
return nil
}
}
}

/**
Expand All @@ -32,8 +75,8 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
/**
Constructs the AsyncNodePlugin
- Parameters:
- handler: The callback that is used to tap into the core `onAsyncNode` hook
exposed to users of the plugin allowing them to supply the replacement node used in the tap callback
- handler: The callback that is used to tap into the core `onAsyncNode` hook
exposed to users of the plugin allowing them to supply the replacement node used in the tap callback
*/
public convenience init(plugins: [JSBasePlugin] = [AsyncNodePluginPlugin()], _ handler: @escaping AsyncHookHandler) {

Expand All @@ -46,53 +89,17 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
super.setup(context: context)

if let pluginRef = pluginRef {
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook(baseValue: pluginRef, name: "onAsyncNode"))
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode"))
}

hooks?.onAsyncNode.tap({ node in
hooks?.onAsyncNode.tap({ node, callback in
// hook value is the original node
guard let asyncHookHandler = self.asyncHookHandler else {
return JSValue()
}

let replacementNode = try await (asyncHookHandler)(node)

switch replacementNode {
case .multiNode(let replacementNodes):
let jsValueArray = replacementNodes.compactMap({ node in
switch node {
case .concrete(let jsValue):
return jsValue
case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
return nil
}
}
})

return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])

case .singleNode(let replacementNode):
switch replacementNode {

case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
break
}
case .concrete(let jsValue):
return jsValue
}
}

return nil
let replacementNode = try await (asyncHookHandler)(node, callback)
return replacementNode.handlerTypeToJSValue(context:context) ?? JSValue()
})
}

Expand All @@ -102,29 +109,29 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
- returns: An array of arguments to construct the plugin
*/
override public func getArguments() -> [Any] {
for plugin in plugins {
plugin.context = self.context
}
for plugin in plugins {
plugin.context = self.context
}

return [["plugins": plugins.map { $0.pluginRef }]]
}
return [["plugins": plugins.map { $0.pluginRef }]]
}

override open func getUrlForFile(fileName: String) -> URL? {
#if SWIFT_PACKAGE
#if SWIFT_PACKAGE
ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)
#else
#else
ResourceUtilities.urlForFile(
name: fileName,
ext: "js",
bundle: Bundle(for: AsyncNodePlugin.self),
pathComponent: "PlayerUIAsyncNodePlugin.bundle"
)
#endif
#endif
}
}

public struct AsyncNodeHook {
public let onAsyncNode: AsyncHook<JSValue>
public let onAsyncNode: AsyncHook2<JSValue, JSValue>
}

/**
Expand Down Expand Up @@ -165,7 +172,7 @@ public struct AssetPlaceholderNode: Encodable {
public struct AsyncNode: Codable, Equatable {
var id: String
var async: Bool = true

public init(id: String) {
self.id = id
}
Expand All @@ -180,15 +187,15 @@ public class AsyncNodePluginPlugin: JSBasePlugin {
}

override open func getUrlForFile(fileName: String) -> URL? {
#if SWIFT_PACKAGE
#if SWIFT_PACKAGE
ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)
#else
#else
ResourceUtilities.urlForFile(
name: fileName,
ext: "js",
bundle: Bundle(for: AsyncNodePluginPlugin.self),
pathComponent: "PlayerUIAsyncNodePlugin.bundle"
)
#endif
#endif
}
}
Loading

0 comments on commit 71c18c7

Please sign in to comment.