Files
jscriptx/caller.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

122 lines
4.0 KiB
Go

package jscriptx
import (
"context"
"github.com/dop251/goja"
)
// Caller 是在 VM 借出期间对脚本函数的操作入口。
//
// 它存在的理由是:有些调用模式没法用 Call/CallInto 表达——典型的是「回调 + next」,
// 需要先看脚本函数声明了几个形参,再决定怎么调它、把哪个 Go 闭包传进去。这些都得在
// 同一次 VM 借出期间完成,因为传给脚本的 Go 闭包只在那段时间里有效。
//
// 拿它写一个自定义的调用约定:
//
// err := target.WithCall(ctx, "handle", func(c jscriptx.Caller) error {
// if c.Arity() == 2 {
// res, err := c.Call(model, next) // next 是 Go 闭包,脚本能直接调
// …
// }
// return nil
// })
//
// 框架自己的回调约定(比如 orm 那套「一元/二元」两种签名)就是这么实现的:
// 先用 Arity 看脚本写了几个形参,再决定怎么调。
//
// Caller 只在 do 回调执行期间有效,别存下来跨调用用。
type Caller interface {
// Arity 返回脚本函数声明的形参个数(JS 函数的 length 属性)。
// 靠它判断脚本写的是哪种形状,脚本就不用额外声明签名。
Arity() int
// Call 调用脚本函数。参数按 goja 的规则转换:Go 对象反射包装成脚本对象,
// Go 函数变成脚本能直接调的函数——「把 next 传给脚本」就是这么实现的。
Call(args ...any) (Result, error)
// Script 返回脚本名,拼错误信息时用得上。
Script() string
}
// Result 是脚本函数一次调用的返回值。
//
// 它只在下一次 Call 之前有效——同一个 Caller 的多次调用复用同一个对象,
// 要留着以后用就先 Value() 或 Into() 取出来。
type Result interface {
// IsEmpty 判断脚本有没有返回东西(undefined 或 null)。
// 「1 个形参、没有返回值」这种纯副作用的写法靠它识别。
IsEmpty() bool
// Value 返回导出成 Go 值的结果。空返回值时是 nil。
Value() any
// Into 把返回值转换进 out 指向的变量(out 必须是非 nil 指针),
// 目标是接口时要求返回值实现它。
Into(out any) error
}
// WithCall 借一个 VM,在借出期间把控制权交给 do。
//
// 超时中断、panic 恢复、错误分类、VM 归还这些都跟普通调用一样,do 里只管发起调用。
// do 返回的错误会被包成 *Error;想让调用方 errors.Is 得到,wrap 一个哨兵错误进去,
// 比如 ErrUnsupportedSignature。
func (s *Script) WithCall(ctx context.Context, fn string, do func(Caller) error) error {
return withCaller(ctx, s, fn, do)
}
// WithCall 同 Script.WithCall,只是在这个实例独占的 VM 上执行。
func (i *Instance) WithCall(ctx context.Context, fn string, do func(Caller) error) error {
return withCaller(ctx, i, fn, do)
}
func withCaller(ctx context.Context, r runner, fn string, do func(Caller) error) error {
return invoke(ctx, r, fn, nil, func(f *frame) error {
return do(&caller{f: f})
})
}
type caller struct {
f *frame
// 复用同一个 result:一次 WithCall 里可能调好几次脚本函数,
// 每次都分配一个返回值对象不划算。所以 Result 只在下一次 Call 之前有效。
res result
}
func (c *caller) Arity() int { return int(c.f.arity()) }
func (c *caller) Script() string { return c.f.script.name }
func (c *caller) Call(args ...any) (Result, error) {
v, err := c.f.callWith(args)
if err != nil {
return nil, err
}
c.res = result{f: c.f, v: v}
return &c.res, nil
}
type result struct {
f *frame
v goja.Value
}
func (r *result) IsEmpty() bool { return empty(r.v) }
func (r *result) Value() any {
if empty(r.v) {
return nil
}
return r.v.Export()
}
func (r *result) Into(out any) error {
if empty(r.v) {
return nil
}
if err := r.f.export(r.v, out); err != nil {
return newError(KindType, r.f.script.name, r.f.name, err,
"返回值无法转换成 %T(拿到的是 %T)", out, r.v.Export())
}
return nil
}