All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.kotlin.com.intuit.playerui.j2v8.bridge.runtime.V8Runtime.kt Maven / Gradle / Ivy

There is a newer version: 0.10.0-next.5
Show newest version
package com.intuit.playerui.j2v8.bridge.runtime

import com.alexii.j2v8debugger.ScriptSourceProvider
import com.eclipsesource.v8.V8
import com.eclipsesource.v8.V8Array
import com.eclipsesource.v8.V8Object
import com.eclipsesource.v8.V8Value
import com.eclipsesource.v8.utils.MemoryManager
import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.PlayerRuntimeException
import com.intuit.playerui.core.bridge.runtime.PlayerRuntimeConfig
import com.intuit.playerui.core.bridge.runtime.PlayerRuntimeContainer
import com.intuit.playerui.core.bridge.runtime.PlayerRuntimeFactory
import com.intuit.playerui.core.bridge.runtime.Runtime
import com.intuit.playerui.core.bridge.runtime.ScriptContext
import com.intuit.playerui.core.bridge.serialization.serializers.playerSerializersModule
import com.intuit.playerui.core.player.PlayerException
import com.intuit.playerui.core.utils.InternalPlayerApi
import com.intuit.playerui.core.utils.await
import com.intuit.playerui.j2v8.V8Null
import com.intuit.playerui.j2v8.V8Primitive
import com.intuit.playerui.j2v8.addPrimitive
import com.intuit.playerui.j2v8.bridge.V8Node
import com.intuit.playerui.j2v8.bridge.serialization.format.J2V8Format
import com.intuit.playerui.j2v8.bridge.serialization.format.J2V8FormatConfiguration
import com.intuit.playerui.j2v8.bridge.serialization.serializers.V8ValueSerializer
import com.intuit.playerui.j2v8.extensions.evaluateInJSThreadBlocking
import com.intuit.playerui.j2v8.extensions.handleValue
import com.intuit.playerui.j2v8.extensions.unlock
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import java.nio.file.Path
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.io.path.createTempDirectory
import kotlin.io.path.pathString

@JvmOverloads
public fun Runtime(runtime: V8, config: J2V8RuntimeConfig = J2V8RuntimeConfig(runtime)): Runtime =
    V8Runtime(config)

public fun Runtime(globalAlias: String? = null, tempDir: Path? = null): Runtime =
    Runtime(V8.createV8Runtime(globalAlias, tempDir?.pathString).unlock())

// TODO: Do a better job of exposing runtime args as Config params to limit the need for these
@JvmOverloads
public fun Runtime(globalAlias: String? = null, tempDirPrefix: String? = null): Runtime =
    Runtime(globalAlias, tempDirPrefix?.let(::createTempDirectory))

internal class V8Runtime(override val config: J2V8RuntimeConfig) : Runtime, ScriptSourceProvider {

    lateinit var v8: V8; private set

    override val dispatcher: ExecutorCoroutineDispatcher = config.executorService.asCoroutineDispatcher()

    override val format: J2V8Format = J2V8Format(
        J2V8FormatConfiguration(
            this,
            playerSerializersModule + SerializersModule {
                contextual(V8Value::class, V8ValueSerializer)
                contextual(V8Object::class, V8ValueSerializer.conform())
                contextual(V8Array::class, V8ValueSerializer.conform())
                contextual(V8Primitive::class, V8ValueSerializer.conform())
                contextual(V8Null::class, V8ValueSerializer.conform())
            },
        ),
    )

    private lateinit var memoryScope: MemoryManager; private set

    override val scope: CoroutineScope by lazy {
        // explicitly not using the JS specific dispatcher to avoid clogging up that thread
        CoroutineScope(Dispatchers.Default + SupervisorJob() + (config.coroutineExceptionHandler ?: EmptyCoroutineContext))
    }

    init {
        // TODO: Uplevel suspension init towards creation point instead of runBlocking
        runBlocking {
            init()
        }
    }

    // Call to initialize V8 -- needs to be invoked for V8 to be set, will acquire the lock for pre-created V8s on the
    // single threaded executor (will fail if another thread has the lock). Otherwise, create a new V8 runtime
    suspend fun init() {
        withContext(dispatcher) {
            v8 = config.runtime?.apply {
                locker.acquire()
            } ?: if (config.debuggable) {
                try {
                    com.alexii.j2v8debugger.V8Debugger.createDebuggableV8Runtime(config.executorService, "debuggableJ2v8", true).await()
                } catch (e: ClassNotFoundException) {
                    throw PlayerRuntimeException("V8Debugger not found. Ensure 'com.github.AlexTrotsenko:j2v8-debugger:0.2.3' is included on your classpath.", e)
                }
            } else {
                V8.createV8Runtime()
            }
            memoryScope = MemoryManager(v8)
        }
    }

    override fun execute(script: String): Any? = v8.evaluateInJSThreadBlocking(runtime) {
        executeScript(script).handleValue(format)
    }

    override fun load(scriptContext: ScriptContext): Unit = v8.evaluateInJSThreadBlocking(runtime) {
        if (config.debuggable) {
            scriptIds.add(scriptContext.id)
            scriptMapping[scriptContext.id] = scriptContext.script
        }
        executeScript(scriptContext.script, scriptContext.id.takeIf { config.debuggable }, 0)
    }

    override fun add(name: String, value: V8Value) {
        v8.evaluateInJSThreadBlocking(runtime) {
            when (value) {
                is V8Primitive -> addPrimitive(name, value)
                else -> add(name, value)
            }
        }
    }

    override fun  serialize(serializer: SerializationStrategy, value: T): Any? = v8.evaluateInJSThreadBlocking(runtime) {
        format.encodeToRuntimeValue(serializer, value).handleValue(format)
    }

    override fun release() {
        // cancel work in runtime scope
        scope.cancel("releasing runtime")
        // swap to dispatcher to release everything
        runBlocking(dispatcher) {
            memoryScope.release()
            v8.release(true)
        }
        // close dispatcher
        dispatcher.close()
    }

    @InternalPlayerApi
    override var checkBlockingThread: Thread.() -> Unit = {}

    private val scriptMapping = mutableMapOf()

    private val scriptIds: MutableSet = mutableSetOf()
    override val allScriptIds: Collection
        get() = scriptIds.toSet()

    override fun getSource(scriptId: String): String = scriptMapping[scriptId] ?: throw PlayerException("Script with name $scriptId not available for debugging, was it loaded?")

    override fun toString(): String = "J2V8"

    // Delegated Node members
    private val backingNode: Node = V8Node(v8, this)

    override val runtime: V8Runtime = this
    override val entries: Set> by backingNode::entries
    override val keys: Set by backingNode::keys
    override val size: Int by backingNode::size
    override val values: Collection by backingNode::values
    override fun containsKey(key: String): Boolean = backingNode.containsKey(key)
    override fun containsValue(value: Any?): Boolean = backingNode.containsValue(value)
    override fun get(key: String): Any? = backingNode[key]
    override fun isEmpty(): Boolean = backingNode.isEmpty()
    override fun  getSerializable(key: String, deserializer: DeserializationStrategy): T? =
        backingNode.getSerializable(key, deserializer)

    override fun  deserialize(deserializer: DeserializationStrategy): T = backingNode.deserialize(deserializer)
    override fun isReleased(): Boolean = backingNode.isReleased()
    override fun isUndefined(): Boolean = backingNode.isUndefined()
    override fun nativeReferenceEquals(other: Any?): Boolean = backingNode.nativeReferenceEquals(other)
    override fun getString(key: String): String? = backingNode.getString(key)
    override fun getInt(key: String): Int? = backingNode.getInt(key)
    override fun getDouble(key: String): Double? = backingNode.getDouble(key)
    override fun getLong(key: String): Long? = backingNode.getLong(key)
    override fun getBoolean(key: String): Boolean? = backingNode.getBoolean(key)
    override fun  getInvokable(key: String, deserializationStrategy: DeserializationStrategy): Invokable? = backingNode.getInvokable(key, deserializationStrategy)
    override fun  getFunction(key: String): Invokable? = backingNode.getFunction(key)
    override fun getList(key: String): List<*>? = backingNode.getList(key)
    override fun getObject(key: String): Node? = backingNode.getObject(key)
}

public object J2V8 : PlayerRuntimeFactory {
    override fun create(block: J2V8RuntimeConfig.() -> Unit): Runtime =
        V8Runtime(J2V8RuntimeConfig().apply(block))

    override fun toString(): String = "J2V8"
}

public data class J2V8RuntimeConfig(
    var runtime: V8? = null,
    private val explicitExecutorService: ExecutorService? = null,
    override var debuggable: Boolean = false,
    override var coroutineExceptionHandler: CoroutineExceptionHandler? = null,
    override var timeout: Long = if (debuggable) Int.MAX_VALUE.toLong() else 5000,
) : PlayerRuntimeConfig() {
    public val executorService: ExecutorService by lazy {
        explicitExecutorService ?: Executors.newSingleThreadExecutor {
            Executors.defaultThreadFactory().newThread(it).apply {
                name = "js-runtime"
            }
        }
    }
}

public class J2V8RuntimeContainer : PlayerRuntimeContainer {
    override val factory: PlayerRuntimeFactory<*> = J2V8

    override fun toString(): String = "J2V8"
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy