Files
jscriptx/invoke.go
T
what 9b3509ae29 refactor: 清掉指向不存在的 Dispatch 的文档,删掉为它留的死导出
Dispatch 在仓库里出现 7 次,全是注释和错误文案,没有任何实现。三处错误文案
写着「需要回调语义请用 Dispatch」——使用者按这句去查会找不到东西,真正该指的
是 WithCall。caller.go 那句还指向不存在的子包 jscriptx/dispatch。

连带删掉四个为它留的导出(全仓库零调用):

  Target                   统一 Script/Instance 的接口。有未导出方法 owner(),
                           外部实现不了;也没有任何函数以它为参数或返回值
  ErrUnsupportedSignature  哨兵错误,库自己从不产生它
  KindSignature            错误分类,全仓库唯一一次出现就是它自己的声明。
                           留着会让写 switch 的人为一个永不出现的分支写代码
  OverlayLoader.Loaders    零调用的 getter,连测试都没有

另外删掉 Instance.IdleFor 和 lastUsed 字段:它是给「空闲回收」用的,而
doc.go 明确写着本库不代管实例生命周期、没有空闲回收——字段注释和包文档直接
对立。代价是每次 Call 白付两次 time.Now() + atomic store。业务侧真要自己回收,
记一个时间戳是一行的事。

caller 的示例原来拿 ErrUnsupportedSignature 当哨兵,改成自己声明一个——
回调签名的约定本来就是调用方定的,哨兵该归调用方。

验证:framework-v2 和 lx-bid 都仍能编译。
2026-09-10 15:15:10 +08:00

327 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jscriptx
import (
"context"
"fmt"
"reflect"
"runtime/debug"
"sync"
"time"
"github.com/dop251/goja"
)
// frame 是一次调用的上下文:拿到了 VM、目标函数,可以真正发起调用。
// 它只在 withCall 的回调里有效,不能存下来跨调用使用。
type frame struct {
script *Script
rt *goja.Runtime
fn goja.Callable
fnVal goja.Value
this goja.Value // 方法调用时绑定的 this(导出实例),默认导出函数为 undefined
name string
}
// arity 返回 JS 函数声明的形参个数(函数对象的 length 属性)。
// WithCall 的回调靠它判断脚本写的是哪种形状,见 Caller.Arity。
func (f *frame) arity() int64 {
return f.fnVal.ToObject(f.rt).Get("length").ToInteger()
}
// call 调用目标函数,this 按 lookup 的结果绑定。
func (f *frame) call(args ...goja.Value) (goja.Value, error) {
return f.fn(f.this, args...)
}
// callWith 用 Go 值直接调用目标函数。
func (f *frame) callWith(args []any) (goja.Value, error) {
jsArgs := make([]goja.Value, len(args))
for i, a := range args {
jsArgs[i] = f.rt.ToValue(a)
}
return f.call(jsArgs...)
}
// export 把脚本返回值转成 Go 值,写进 target 指向的变量。
func (f *frame) export(v goja.Value, target any) error {
return f.rt.ExportTo(v, target)
}
// empty 判断脚本返回值是不是"什么都没返回"。
func empty(v goja.Value) bool {
return v == nil || goja.IsUndefined(v) || goja.IsNull(v)
}
// Call 调用脚本里的函数,返回值导出成 Go 值(JS 对象变 map[string]any,数组变 []any
// 传进去的 Go 对象原样回来)。fn 传 DefaultFunc 表示调用脚本自身求值出的那个函数。
//
// 脚本返回函数/闭包会被拒绝:那种值只在 VM 内部有效,VM 归还池子后再调用会出问题。
// 需要把脚本函数当回调用,走 WithCall——在 VM 借出期间调,别把函数带出来。
func (s *Script) Call(ctx context.Context, fn string, args ...any) (any, error) {
return callAny(ctx, s, fn, args)
}
// CallInto 调用脚本里的函数,并把返回值转换进 out 指向的变量(out 必须是非 nil 指针),
// 相当于带类型的 Call:目标是 int 就按 int 转,是某个接口就要求返回值实现它。
func (s *Script) CallInto(ctx context.Context, fn string, out any, args ...any) error {
return callInto(ctx, s, fn, out, args)
}
// callAny 是 Call 的共用实现,*Script 和 *Session 都走这里。
func callAny(ctx context.Context, r runner, fn string, args []any) (any, error) {
var out any
err := invoke(ctx, r, fn, args, func(f *frame) error {
res, err := f.callWith(args)
if err != nil {
return err
}
res, err = unwrapPromise(f, res, r.owner().name, fn)
if err != nil {
return err
}
if empty(res) {
return nil
}
if _, isFunc := goja.AssertFunction(res); isFunc {
return newError(KindType, r.owner().name, fn, ErrValueEscape,
"返回值是 JS 函数,只在脚本内部有效;需要回调语义请用 WithCall")
}
out = res.Export()
return nil
})
return out, err
}
// callInto 是 CallInto 的共用实现。
func callInto(ctx context.Context, r runner, fn string, out any, args []any) error {
name := r.owner().name
rv := reflect.ValueOf(out)
if !rv.IsValid() || rv.Kind() != reflect.Pointer || rv.IsNil() {
return newError(KindType, name, fn, nil, "CallInto 的 out 必须是非 nil 指针,当前是 %T", out)
}
// 导出成 Go 函数意味着把一个绑定在 VM 上的闭包带出脚本边界,
// 而这个 VM 马上就要还回池子给别的请求用了。
if rv.Type().Elem().Kind() == reflect.Func {
return newError(KindType, name, fn, ErrValueEscape,
"不能把脚本函数导出成 Go 函数(VM 归还池子后它就失效了);需要回调语义请用 WithCall")
}
return invoke(ctx, r, fn, args, func(f *frame) error {
res, err := f.callWith(args)
if err != nil {
return err
}
res, err = unwrapPromise(f, res, name, fn)
if err != nil {
return err
}
if empty(res) {
return nil
}
if err := f.export(res, out); err != nil {
return newError(KindType, name, fn, err, "返回值无法转换成 %T", out)
}
return nil
})
}
// runner 抽象"这次调用用哪个 VM、用完怎么处理":*Script 从池里借还,
// *Session 独占一个 VMacquire 会一直持锁到 finish,保证同一会话串行执行)。
type runner interface {
owner() *Script
acquire(ctx context.Context) (*vmHandle, error)
finish(inst *vmHandle, healthy bool)
}
func (s *Script) owner() *Script { return s }
func (s *Script) acquire(ctx context.Context) (*vmHandle, error) { return s.borrow(ctx) }
func (s *Script) finish(inst *vmHandle, healthy bool) { s.release(inst, healthy) }
// invoke 是所有脚本调用的骨架:取 VM → 找函数 → 装超时哨兵 → 兜 panic → 分类错误 → 交还 VM。
// do 里只管发起调用和处理返回值,异常处理交给这里。
func invoke(ctx context.Context, r runner, fn string, args []any, do func(*frame) error) (err error) {
s := r.owner()
if s.closed.Load() {
return newError(KindClosed, s.name, fn, ErrClosed, "脚本已关闭")
}
if ctx == nil {
ctx = context.Background()
}
inst, err := r.acquire(ctx)
if err != nil {
return err
}
healthy := true
// defer 是后进先出:recover 最先跑(把 healthy 置回 false),
// 然后 stop 清掉可能迟到的中断标志,最后才交还 VM。
defer func() { r.finish(inst, healthy) }()
callable, fnVal, this, ok := inst.lookup(fn)
if !ok {
return newError(KindNotFound, s.name, fn, ErrFuncNotFound, "%s", s.notFoundHint(inst))
}
stop := guard(ctx, inst.rt, s.engine.timeout)
defer stop()
defer func() {
if r := recover(); r != nil {
healthy = false
err = s.panicError(fn, args, r)
}
}()
f := &frame{script: s, rt: inst.rt, fn: callable, fnVal: fnVal, this: this, name: fn}
if e := do(f); e != nil {
err = classify(e, s.name, fn, args)
healthy = !fatal(err)
return err
}
return nil
}
// panicError 把 recover 到的 panic 包成带现场的错误。脚本里的类型错误、
// 注入进去的 Go 方法内部 panic,都会走到这里,不会掀翻调用方的 goroutine。
func (s *Script) panicError(fn string, args []any, r any) *Error {
return &Error{
Kind: KindPanic,
Script: s.name,
Func: fn,
Msg: fmt.Sprintf("脚本执行过程中发生 panic: %v", r),
Args: summarize(args),
GoStack: string(debug.Stack()),
Cause: toError(r),
}
}
// interruptGuard 保证"中断"和"收尾"两件事不会打架:收尾之后再迟到的中断信号必须被丢掉,
// 否则它会毒死下一次用到这个 VM 的调用。
type interruptGuard struct {
mu sync.Mutex
stopped bool
rt *goja.Runtime
}
func (g *interruptGuard) fire(reason error) {
g.mu.Lock()
defer g.mu.Unlock()
if g.stopped {
return
}
g.rt.Interrupt(reason)
}
// stop 关掉哨兵并清掉可能已经发出的中断标志。返回后保证不会再有新的中断落到这个 VM 上。
func (g *interruptGuard) stop() {
g.mu.Lock()
g.stopped = true
g.mu.Unlock()
// 到这里要么 fire 已经跑完(中断标志由下面清掉),要么它以后永远不会再发。
g.rt.ClearInterrupt()
}
// guard 装一个中断哨兵:超时或者调用方 context 取消时,从另一个 goroutine 调
// Interrupt 打断正在执行的脚本(死循环也能断掉)。返回的函数负责收尾。
func guard(ctx context.Context, rt *goja.Runtime, timeout time.Duration) func() {
watchCtx := ctx.Done() != nil
if timeout <= 0 && !watchCtx {
return func() {}
}
g := &interruptGuard{rt: rt}
if !watchCtx {
// 快路径:没有 context 取消要盯,一个定时器就够了,不用为每次调用起 goroutine。
// MQTT 消息级这种高频调用走的就是这条路。
timer := time.AfterFunc(timeout, func() { g.fire(context.DeadlineExceeded) })
return func() {
g.stop()
timer.Stop()
}
}
var cancel context.CancelFunc
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout)
}
done := make(chan struct{})
exited := make(chan struct{})
go func() {
defer close(exited)
select {
case <-ctx.Done():
g.fire(ctx.Err())
case <-done:
}
}()
return func() {
close(done)
<-exited
if cancel != nil {
cancel()
}
g.stop()
}
}
// unwrapPromise 把 async 方法返回的 Promise 解开。
//
// goja 在调用返回前会把 microtask 队列跑干净,所以只要脚本里没有真正需要等待外部
// 事件的操作(goja 没有事件循环,也没有 setTimeout),Promise 到这里已经是完成态了。
// 解开它有两个理由:一是 *goja.Promise 属于引擎类型,不该出现在本库的返回值里;
// 二是调用方拿到一个未解包的 Promise 什么也做不了。
func unwrapPromise(f *frame, v goja.Value, script, fn string) (goja.Value, error) {
p, ok := promiseOf(v)
if !ok {
return v, nil
}
switch p.State() {
case goja.PromiseStateFulfilled:
return p.Result(), nil
case goja.PromiseStateRejected:
reason := p.Result()
e := &Error{
Kind: KindRuntime,
Script: script,
Func: fn,
Msg: "async 方法的 Promise 被 reject",
Cause: ErrPromiseRejected,
}
if reason != nil {
e.Msg = reason.String()
e.Value = reason.Export()
}
return nil, e
default:
return nil, newError(KindRuntime, script, fn, ErrPromisePending,
"async 方法返回的 Promise 一直没完成。goja 没有事件循环,"+
"脚本里没法等待真正的异步操作(定时器、网络、IO);"+
"异步的活交给 Go 侧做,脚本只写同步逻辑")
}
}
// promiseOf 判断一个返回值是不是 Promise。
//
// *goja.Promise 只能从 Export() 拿到,但对普通对象调 Export 会把整个对象转成 map,
// 那个开销不能加在每次调用上。先看有没有 then 属性——一次廉价的属性查找就能排除掉
// 绝大多数返回值,只有 thenable 才走到 Export。
func promiseOf(v goja.Value) (*goja.Promise, bool) {
obj, ok := v.(*goja.Object)
if !ok {
return nil, false
}
if then := obj.Get("then"); then == nil || goja.IsUndefined(then) {
return nil, false
}
p, ok := obj.Export().(*goja.Promise)
return p, ok
}