Skip to content

Commit 71c18c7

Browse files
Merge pull request #488 from player-ui/jvm-async-node-ability-to-remove-resolved-async-node
jvm and ios-async-node-ability-to-remove-resolved-async-node
2 parents ccc758a + 32b9c57 commit 71c18c7

File tree

9 files changed

+659
-173
lines changed

9 files changed

+659
-173
lines changed

ios/core/Sources/Types/Hooks/Hook.swift

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// Hook.swift
3-
//
3+
//
44
//
55
// Created by Borawski, Harris on 2/12/20.
66
//
@@ -41,7 +41,7 @@ public class Hook<T>: BaseJSHook where T: CreatedFromJSValue {
4141
we receive the event in the native runtime
4242

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

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

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

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

153153
- parameters:
154-
- hook: A function to run when the JS hook is fired
154+
- hook: A function to run when the JS hook is fired
155155
*/
156156
public func tap(_ hook: @escaping AsyncHookHandler) {
157157
let tapMethod: @convention(block) (JSValue?) -> JSValue = { value in
158-
guard
159-
let val = value,
160-
let hookValue = T.createInstance(value: val) as? T
161-
else { return JSValue() }
162-
163-
let promise =
164-
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
165-
Task {
166-
let result = try await hook(hookValue)
167-
DispatchQueue.main.async {
168-
resolve(result as Any)
169-
}
170-
}
171-
})
172-
173-
return promise ?? JSValue()
174-
}
175-
176-
self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
177-
}
158+
guard
159+
let val = value,
160+
let hookValue = T.createInstance(value: val) as? T
161+
else { return JSValue() }
162+
163+
let promise =
164+
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
165+
Task {
166+
let result = try await hook(hookValue)
167+
DispatchQueue.main.async {
168+
resolve(result as Any)
169+
}
170+
}
171+
})
172+
173+
return promise ?? JSValue()
174+
}
175+
176+
self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
177+
}
178+
}
179+
180+
/**
181+
This class represents an object in the JS runtime that can be tapped into
182+
to receive JS events that has 2 parameters and
183+
returns a promise that resolves when the asynchronous task is completed
184+
*/
185+
public class AsyncHook2<T, U>: BaseJSHook where T: CreatedFromJSValue, U: CreatedFromJSValue {
186+
private var handler: AsyncHookHandler?
187+
188+
public typealias AsyncHookHandler = (T, U) async throws -> JSValue?
189+
190+
/**
191+
Attach a closure to the hook, so when the hook is fired in the JS runtime
192+
we receive the event in the native runtime
193+
194+
- parameters:
195+
- hook: A function to run when the JS hook is fired
196+
*/
197+
public func tap(_ hook: @escaping AsyncHookHandler) {
198+
let tapMethod: @convention(block) (JSValue?,JSValue?) -> JSValue = { value, value2 in
199+
guard
200+
let val = value,
201+
let val2 = value2,
202+
let hookValue = T.createInstance(value: val) as? T,
203+
let hookValue2 = U.createInstance(value: val2) as? U
204+
else { return JSValue() }
205+
206+
207+
let promise =
208+
JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in
209+
Task {
210+
let result = try await hook(hookValue, hookValue2)
211+
DispatchQueue.main.async {
212+
resolve(result as Any)
213+
}
214+
}
215+
})
216+
217+
return promise ?? JSValue()
218+
}
219+
220+
self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any])
221+
}
178222
}
223+

jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/Promise.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public val Runtime<*>.Promise: Promise.Api get() = getObject("Promise")?.let { p
178178
} ?: throw PlayerRuntimeException("'Promise' not defined in runtime")
179179

180180
/** Helper to bridge complex [Promise] logic with the JS promise constructor */
181-
public fun <T : Any> Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise {
181+
public fun <T : Any?> Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise {
182182
val key = "promiseHandler_${UUID.randomUUID().toString().replace("-", "")}"
183183
add(key) { resolve: Invokable<Any?>, reject: Invokable<Any?> ->
184184
runtime.scope.launch {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.intuit.playerui.core.bridge.hooks
2+
3+
import com.intuit.hooks.AsyncParallelBailHook
4+
import com.intuit.hooks.BailResult
5+
import com.intuit.hooks.HookContext
6+
import com.intuit.playerui.core.bridge.Node
7+
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
8+
import kotlinx.coroutines.ExperimentalCoroutinesApi
9+
import kotlinx.serialization.KSerializer
10+
import kotlinx.serialization.Serializable
11+
12+
@OptIn(ExperimentalCoroutinesApi::class)
13+
@Serializable(with = NodeAsyncParallelBailHook2.Serializer::class)
14+
public class NodeAsyncParallelBailHook2<T1, T2, R : Any?>(
15+
override val node: Node,
16+
serializer1: KSerializer<T1>,
17+
serializer2: KSerializer<T2>,
18+
) : AsyncParallelBailHook<suspend (HookContext, T1, T2) -> BailResult<R>, R>(), AsyncNodeHook<R> {
19+
20+
init {
21+
init(serializer1, serializer2)
22+
}
23+
24+
override suspend fun callAsync(context: HookContext, serializedArgs: Array<Any?>): R {
25+
require(serializedArgs.size == 2) { "Expected exactly two arguments, but got ${serializedArgs.size}" }
26+
val (p1, p2) = serializedArgs
27+
val result = call(10) { f, _ ->
28+
f(context, p1 as T1, p2 as T2)
29+
} as R
30+
return result
31+
}
32+
33+
internal class Serializer<T1, T2, R : Any>(
34+
private val serializer1: KSerializer<T1>,
35+
private val serializer2: KSerializer<T2>,
36+
`_`: KSerializer<R>,
37+
) : NodeWrapperSerializer<NodeAsyncParallelBailHook2<T1, T2, R>>({
38+
NodeAsyncParallelBailHook2(it, serializer1, serializer2)
39+
})
40+
}

jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeHook.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal interface NodeHook<R> : NodeWrapper {
3939
fun call(context: HookContext, serializedArgs: Array<Any?>): R
4040
}
4141

42-
internal interface AsyncNodeHook<R : Any> : NodeHook<Promise> {
42+
internal interface AsyncNodeHook<R : Any?> : NodeHook<Promise> {
4343
override fun call(context: HookContext, serializedArgs: Array<Any?>): Promise = node.runtime.Promise { resolve, reject ->
4444
val result = callAsync(context, serializedArgs)
4545
resolve(result)

plugins/async-node/ios/Sources/AsyncNodePlugin.swift

Lines changed: 66 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,59 @@
77

88
import Foundation
99
import JavaScriptCore
10-
11-
#if SWIFT_PACKAGE
1210
import PlayerUI
13-
#endif
1411

15-
public typealias AsyncHookHandler = (JSValue) async throws -> AsyncNodeHandlerType
12+
public typealias AsyncHookHandler = (JSValue, JSValue) async throws -> AsyncNodeHandlerType
1613

1714
public enum AsyncNodeHandlerType {
1815
case multiNode([ReplacementNode])
1916
case singleNode(ReplacementNode)
17+
case emptyNode
18+
}
19+
20+
/// Extension for `ReplacementNode` to convert it to a `JSValue` in a given `JSContext`.
21+
public extension ReplacementNode {
22+
/// Converts the `ReplacementNode` to a `JSValue` in the provided `JSContext`.
23+
///
24+
/// - Parameter context: The `JSContext` in which the `JSValue` will be created.
25+
/// - Returns: A `JSValue` representing the `ReplacementNode`, or `nil` if the conversion fails.
26+
func toJSValue(context: JSContext) -> JSValue? {
27+
switch self {
28+
case .encodable(let encodable):
29+
let encoder = JSONEncoder()
30+
do {
31+
let res = try encoder.encode(encodable)
32+
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
33+
} catch {
34+
return nil
35+
}
36+
case .concrete(let jsValue):
37+
return jsValue
38+
}
39+
}
40+
}
41+
42+
/// Extension for `AsyncNodeHandlerType` to convert it to a `JSValue` in a given `JSContext`.
43+
public extension AsyncNodeHandlerType {
44+
/// Converts the `AsyncNodeHandlerType` to a `JSValue` in the provided `JSContext`.
45+
///
46+
/// - Parameter context: The `JSContext` in which the `JSValue` will be created.
47+
/// - Returns: A `JSValue` representing the `AsyncNodeHandlerType`, or `nil` if the conversion fails.
48+
func handlerTypeToJSValue(context: JSContext) -> JSValue? {
49+
switch self {
50+
case .multiNode(let replacementNodes):
51+
let jsValueArray = replacementNodes.compactMap {
52+
$0.toJSValue(context: context)
53+
}
54+
return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])
55+
56+
case .singleNode(let replacementNode):
57+
return replacementNode.toJSValue(context: context)
58+
59+
case .emptyNode:
60+
return nil
61+
}
62+
}
2063
}
2164

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

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

4891
if let pluginRef = pluginRef {
49-
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook(baseValue: pluginRef, name: "onAsyncNode"))
92+
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode"))
5093
}
5194

52-
hooks?.onAsyncNode.tap({ node in
95+
hooks?.onAsyncNode.tap({ node, callback in
5396
// hook value is the original node
5497
guard let asyncHookHandler = self.asyncHookHandler else {
5598
return JSValue()
5699
}
57100

58-
let replacementNode = try await (asyncHookHandler)(node)
59-
60-
switch replacementNode {
61-
case .multiNode(let replacementNodes):
62-
let jsValueArray = replacementNodes.compactMap({ node in
63-
switch node {
64-
case .concrete(let jsValue):
65-
return jsValue
66-
case .encodable(let encodable):
67-
let encoder = JSONEncoder()
68-
do {
69-
let res = try encoder.encode(encodable)
70-
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
71-
} catch {
72-
return nil
73-
}
74-
}
75-
})
76-
77-
return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])
78-
79-
case .singleNode(let replacementNode):
80-
switch replacementNode {
81-
82-
case .encodable(let encodable):
83-
let encoder = JSONEncoder()
84-
do {
85-
let res = try encoder.encode(encodable)
86-
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
87-
} catch {
88-
break
89-
}
90-
case .concrete(let jsValue):
91-
return jsValue
92-
}
93-
}
94-
95-
return nil
101+
let replacementNode = try await (asyncHookHandler)(node, callback)
102+
return replacementNode.handlerTypeToJSValue(context:context) ?? JSValue()
96103
})
97104
}
98105

@@ -102,29 +109,29 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
102109
- returns: An array of arguments to construct the plugin
103110
*/
104111
override public func getArguments() -> [Any] {
105-
for plugin in plugins {
106-
plugin.context = self.context
107-
}
112+
for plugin in plugins {
113+
plugin.context = self.context
114+
}
108115

109-
return [["plugins": plugins.map { $0.pluginRef }]]
110-
}
116+
return [["plugins": plugins.map { $0.pluginRef }]]
117+
}
111118

112119
override open func getUrlForFile(fileName: String) -> URL? {
113-
#if SWIFT_PACKAGE
120+
#if SWIFT_PACKAGE
114121
ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)
115-
#else
122+
#else
116123
ResourceUtilities.urlForFile(
117124
name: fileName,
118125
ext: "js",
119126
bundle: Bundle(for: AsyncNodePlugin.self),
120127
pathComponent: "PlayerUIAsyncNodePlugin.bundle"
121128
)
122-
#endif
129+
#endif
123130
}
124131
}
125132

126133
public struct AsyncNodeHook {
127-
public let onAsyncNode: AsyncHook<JSValue>
134+
public let onAsyncNode: AsyncHook2<JSValue, JSValue>
128135
}
129136

130137
/**
@@ -165,7 +172,7 @@ public struct AssetPlaceholderNode: Encodable {
165172
public struct AsyncNode: Codable, Equatable {
166173
var id: String
167174
var async: Bool = true
168-
175+
169176
public init(id: String) {
170177
self.id = id
171178
}
@@ -180,15 +187,15 @@ public class AsyncNodePluginPlugin: JSBasePlugin {
180187
}
181188

182189
override open func getUrlForFile(fileName: String) -> URL? {
183-
#if SWIFT_PACKAGE
190+
#if SWIFT_PACKAGE
184191
ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)
185-
#else
192+
#else
186193
ResourceUtilities.urlForFile(
187194
name: fileName,
188195
ext: "js",
189196
bundle: Bundle(for: AsyncNodePluginPlugin.self),
190197
pathComponent: "PlayerUIAsyncNodePlugin.bundle"
191198
)
192-
#endif
199+
#endif
193200
}
194201
}

0 commit comments

Comments
 (0)