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 都仍能编译。
122 lines
4.0 KiB
Go
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
|
|
}
|