2026-05-30 14:21:43 +08:00
|
|
|
package js
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-05-30 15:33:57 +08:00
|
|
|
"log"
|
2026-05-30 14:21:43 +08:00
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/jsmod"
|
|
|
|
|
"github.com/dop251/goja"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type vmInstance struct {
|
|
|
|
|
runtime *goja.Runtime
|
|
|
|
|
version int32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
globalVersion int32
|
|
|
|
|
scripts []string
|
|
|
|
|
scriptsMu sync.RWMutex
|
|
|
|
|
|
|
|
|
|
pool = sync.Pool{
|
|
|
|
|
New: func() any {
|
|
|
|
|
return &vmInstance{
|
|
|
|
|
runtime: createNewRuntime(),
|
|
|
|
|
version: 0,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func createNewRuntime() *goja.Runtime {
|
|
|
|
|
vm := goja.New()
|
|
|
|
|
|
|
|
|
|
// 1. Inject Native Modules from jsmod
|
|
|
|
|
goObj := vm.NewObject()
|
|
|
|
|
_ = vm.Set("go", goObj)
|
|
|
|
|
|
|
|
|
|
modules := jsmod.GetModules()
|
2026-05-30 15:33:57 +08:00
|
|
|
for modName, mod := range modules {
|
2026-05-30 14:21:43 +08:00
|
|
|
modObj := vm.NewObject()
|
2026-05-30 15:33:57 +08:00
|
|
|
for name, val := range mod.Exports {
|
|
|
|
|
isUnsafe := mod.UnsafeList[name]
|
2026-05-30 14:21:43 +08:00
|
|
|
if reflectType := fmt.Sprintf("%T", val); reflectType == "func" || (len(reflectType) > 4 && reflectType[:4] == "func") {
|
2026-05-30 15:33:57 +08:00
|
|
|
_ = modObj.Set(name, wrapGoFunc(vm, val, isUnsafe))
|
2026-05-30 14:21:43 +08:00
|
|
|
} else {
|
|
|
|
|
_ = modObj.Set(name, vm.ToValue(val))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ = goObj.Set(modName, modObj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return vm
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define adds JS code to the global registry and increments the version.
|
|
|
|
|
// All VMs in the pool will eventually synchronize to this version.
|
|
|
|
|
func Define(code string) {
|
|
|
|
|
scriptsMu.Lock()
|
|
|
|
|
defer scriptsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
scripts = append(scripts, code)
|
|
|
|
|
atomic.AddInt32(&globalVersion, 1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// CallOption allows configuring the JS execution environment.
|
|
|
|
|
type CallOption func(vm *goja.Runtime)
|
|
|
|
|
|
|
|
|
|
// WithSafeMode enables or disables safe mode for the call.
|
|
|
|
|
func WithSafeMode(enabled bool) CallOption {
|
|
|
|
|
return func(vm *goja.Runtime) {
|
|
|
|
|
_ = vm.Set("__safeMode__", enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithLogger injects a custom logger for the call.
|
|
|
|
|
func WithLogger(logger *log.Logger) CallOption {
|
|
|
|
|
return func(vm *goja.Runtime) {
|
|
|
|
|
_ = vm.Set("__logger__", vm.ToValue(logger))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 14:21:43 +08:00
|
|
|
// Call executes a JS function from the pool.
|
|
|
|
|
// It automatically synchronizes the VM to the latest version.
|
2026-05-30 15:33:57 +08:00
|
|
|
func Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
2026-05-30 14:21:43 +08:00
|
|
|
instance := pool.Get().(*vmInstance)
|
|
|
|
|
defer pool.Put(instance)
|
|
|
|
|
|
|
|
|
|
vm := instance.runtime
|
|
|
|
|
|
|
|
|
|
// 1. Synchronize scripts if version is behind
|
|
|
|
|
currentGlobalVersion := atomic.LoadInt32(&globalVersion)
|
|
|
|
|
if instance.version < currentGlobalVersion {
|
|
|
|
|
scriptsMu.RLock()
|
|
|
|
|
for i := int(instance.version); i < len(scripts); i++ {
|
|
|
|
|
_, err := vm.RunString(scripts[i])
|
|
|
|
|
if err != nil {
|
|
|
|
|
scriptsMu.RUnlock()
|
|
|
|
|
return nil, fmt.Errorf("js.sync error at script %d: %w", i, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
instance.version = currentGlobalVersion
|
|
|
|
|
scriptsMu.RUnlock()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// 2. Set Context and default state
|
2026-05-30 14:21:43 +08:00
|
|
|
_ = vm.Set("__ctx__", vm.ToValue(ctx))
|
2026-05-30 15:33:57 +08:00
|
|
|
_ = vm.Set("__safeMode__", true) // Default is safe
|
|
|
|
|
_ = vm.Set("__logger__", goja.Undefined())
|
|
|
|
|
|
|
|
|
|
// Apply Options
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(vm)
|
|
|
|
|
}
|
2026-05-30 14:21:43 +08:00
|
|
|
|
|
|
|
|
// 3. Get and Call JS Function
|
|
|
|
|
fnVal := vm.Get(funcName)
|
|
|
|
|
if fnVal == nil || goja.IsUndefined(fnVal) {
|
|
|
|
|
return nil, fmt.Errorf("js.Call: function '%s' not found", funcName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
callable, ok := goja.AssertFunction(fnVal)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("js.Call: '%s' is not a function", funcName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsArgs := make([]goja.Value, len(args))
|
|
|
|
|
for i, arg := range args {
|
|
|
|
|
jsArgs[i] = vm.ToValue(arg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Execution with error capture
|
|
|
|
|
var result goja.Value
|
|
|
|
|
var err error
|
|
|
|
|
func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
err = fmt.Errorf("js panic: %v", r)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
result, err = callable(goja.Undefined(), jsArgs...)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.Export(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FuncList returns the list of all defined JS function names.
|
|
|
|
|
func FuncList() []string {
|
|
|
|
|
scriptsMu.RLock()
|
|
|
|
|
defer scriptsMu.RUnlock()
|
|
|
|
|
return []string{}
|
|
|
|
|
}
|