Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsyncNodePlugin- use named export, port iOS plugin #295

Merged
merged 8 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions PlayerUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ and display it as a SwiftUI view comprised of registered assets.
# </PACKAGES>

# <PLUGINS>

s.subspec 'AsyncNodePlugin' do |plugin|
plugin.dependency 'PlayerUI/Core'
plugin.source_files = 'ios/plugins/AsyncNodePlugin/Sources/**/*'
plugin.resource_bundles = {
'AsyncNodePlugin' => ['ios/plugins/AsyncNodePlugin/Resources/**/*.js']
}
end

s.subspec 'PrintLoggerPlugin' do |plugin|
plugin.dependency 'PlayerUI/Core'
plugin.source_files = 'ios/plugins/PrintLoggerPlugin/Sources/**/*'
Expand Down
2 changes: 1 addition & 1 deletion docs/site/pages/plugins/metrics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ import { BeaconPluginPlugin } from '@player-ui/beacon-plugin';

class MyBeaconPluginPlugin implements BeaconPluginPlugin {
apply(beaconPlugin: BeaconPlugin) {
beaconPlugin.hooks.buildBeacon.tapPromise(
beaconPlugin.hooks.buildBeacon.tap(
{ name: 'my-beacon-plugin', context: true } as Tap,
async (context, beacon) => {
const { renderTime } =
Expand Down
12 changes: 12 additions & 0 deletions generated.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def PlayerUI(
"ENABLE_TESTING_SEARCH_PATHS": "YES",
},
srcs = glob([
"ios/plugins/AsyncNodePlugin/Sources/**/*.h",
"ios/plugins/AsyncNodePlugin/Sources/**/*.hh",
"ios/plugins/AsyncNodePlugin/Sources/**/*.m",
"ios/plugins/AsyncNodePlugin/Sources/**/*.mm",
"ios/plugins/AsyncNodePlugin/Sources/**/*.swift",
"ios/plugins/AsyncNodePlugin/Sources/**/*.c",
"ios/plugins/AsyncNodePlugin/Sources/**/*.cc",
"ios/plugins/AsyncNodePlugin/Sources/**/*.cpp",
"ios/plugins/BaseBeaconPlugin/Sources/**/*.h",
"ios/plugins/BaseBeaconPlugin/Sources/**/*.hh",
"ios/plugins/BaseBeaconPlugin/Sources/**/*.m",
Expand Down Expand Up @@ -213,6 +221,10 @@ def PlayerUI(
"ios/plugins/TypesProviderPlugin/Sources/**/*.cpp",
]),
resource_bundles = {
"AsyncNodePlugin": glob(
["ios/plugins/AsyncNodePlugin/Resources/**/*.js"],
exclude_directories = 0,
),
"BaseBeaconPlugin": glob(
["ios/plugins/BaseBeaconPlugin/Resources/**/*.js"],
exclude_directories = 0,
Expand Down
2 changes: 1 addition & 1 deletion ios/packages/core/Sources/CorePlugins/JSBasePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ open class JSBasePlugin {
*/
private func getPlugin(context: JSContext, fileName: String, pluginName: String, arguments: [Any] = []) -> JSValue {
guard
let plugin = context.getClassReference(pluginName, load: {loadJSResource(into: $0, fileName: fileName)}),
let plugin = context.getClassReference(pluginName, load: {loadJSResource(into: $0, fileName: fileName)}), !plugin.isUndefined,
let pluginValue = plugin.construct(withArguments: arguments)
else {
fatalError("Unable To Construct \(pluginName)")
Expand Down
2 changes: 1 addition & 1 deletion ios/packages/core/Sources/Player/HeadlessPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public extension HeadlessPlayer {
let warn = JSValue(object: logger.getJSLogFor(level: .warning), in: context)
let error = JSValue(object: logger.getJSLogFor(level: .error), in: context)
for plugin in plugins {
if let plugin = plugin as? JSBasePlugin {
if let plugin = plugin as? JSBasePlugin, plugin.context != context {
plugin.context = context
}
}
Expand Down
40 changes: 40 additions & 0 deletions ios/packages/core/Sources/Types/Hooks/Hook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,43 @@ public class Hook2<T, U>: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom
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
and returns a promise that resolves when the asynchronous task is completed
*/
public class AsyncHook<T>: BaseJSHook where T: CreatedFromJSValue {
private var handler: AsyncHookHandler?

public typealias AsyncHookHandler = (T) 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 = { 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])
}
}
1 change: 1 addition & 0 deletions ios/packages/swiftui/Sources/SwiftUIPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public struct SwiftUIPlayer: View, HeadlessPlayer {
}

let context: JSContext = contextBuilder()

let allPlugins = plugins + [partialMatchPlugin]
guard let playerValue = player.setupPlayer(context: context, plugins: allPlugins) else {
return logger.e("Failed to load player")
Expand Down
142 changes: 142 additions & 0 deletions ios/plugins/AsyncNodePlugin/Sources/AsyncNodePlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//
// AsyncNodePlugin.swift
// PlayerUI
//
// Created by Zhao Xia Wu on 2024-02-05.
//

import Foundation
import JavaScriptCore

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

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

/**
Wraps the core AsyncNodePlugin and taps into the `onAsyncNode` hook to allow asynchronous replacement of the node object that contains `async`
*/
public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
public var hooks: AsyncNodeHook?

private var asyncHookHandler: AsyncHookHandler?

/**
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
*/
public convenience init(_ handler: @escaping AsyncHookHandler) {
self.init(fileName: "async-node-plugin.prod", pluginName: "AsyncNodePlugin.AsyncNodePlugin")
self.asyncHookHandler = handler
}

override public func setup(context: JSContext) {
super.setup(context: context)

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

hooks?.onAsyncNode.tap({ node 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
})
}

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

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

/**
Replacement node that the callback of this plugin expects, users can supply either a JSValue or an Encodable object that gets converted to a JSValue in the `setup`
*/
public enum ReplacementNode: Encodable {
case concrete(JSValue)
case encodable(Encodable)

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()

switch self {
case .encodable(let value):
try container.encode(value)
case .concrete( _):
break
}
}
}

public struct AssetPlaceholderNode: Encodable {
public enum CodingKeys: String, CodingKey {
case asset
}

var asset: Encodable
public init(asset: Encodable) {
self.asset = asset
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(asset, forKey: .asset)
}
}

public struct AsyncNode: Codable, Equatable {
var id: String
var async: Bool = true

public init(id: String) {
self.id = id
}
}
Loading