约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.go。
engine.go 355 → 231 行。原来混了三件不相干的事
engine_globals.go ← bind / lazyGlobal / freeze / defineReadOnly(96 行)
它是「Go 值 → 只读 JS 全局」的转换层,跟脚本缓存毫无关系
engine_console.go ← console.go,跟上面是同一主题
script.go 325 → 100 行,只留公开方法
script_vm.go ← VM 的取、还、装载。上个 commit 合一的三条路径现在住一起
script_static.go ← static.go
errors.go 364 → 150 行
errors_goja.go ← goja 错误的翻译层
errors_hints.go ← missingGlobalHint,一份 JS 运行时知识库,跟错误分类是
两回事;拆出来之后 missing_global_test.go 才有对应源文件
bundle_finalize.go ← esmwrap.go
bundle.go 收下 validIdent / hashVersion(原来住在 engine.go)
Engine.New 原来排在所有私有函数之后,挪到导出方法那一段。
classify 83 → 54 行:四个 errors.As 分支各手搓一个 8 字段的 &Error{},脚本上下文
那三行重复了 4 遍,抽出 gojaError 构造器。
顺带修四处注释漂移:
Session/会话 代码里叫 Instance,注释里大面积残留。engine_globals.go 那条
「注入会话全局对象失败」还是用户可见文案,而公开 API 里根本
没有「会话」这个概念
Extension 文档示例写 Module() string,接口是 Module() (path, source string)。
这是唯一一段教人写扩展的文档,照抄编译不过
doc.go 的 freeze 说白名单「逐层拷贝成只读对象,不会跨 VM 共享可变的 Go map」,
但那只对 map[string]any 成立。结构体指针和 slice 是**共享同一个
对象**的——扩展走的正是这条路,不该被当成隔离保证
160 lines
5.2 KiB
Go
160 lines
5.2 KiB
Go
package jscriptx
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"time"
|
|
)
|
|
|
|
// 作用域通过 context 传递:它只携带一段业务流程里要共享的东西(扩展和全局对象),
|
|
// 不管任何生命周期——实例的生死由你 Close 决定。
|
|
//
|
|
// st := jscriptx.NewStore()
|
|
// ctx = jscriptx.WithScope(ctx, jscriptx.ScopeExtensions(st))
|
|
//
|
|
// obj, err := resCtrl.New(ctx, arg) // 从 ctx 拿扩展和全局
|
|
// defer obj.Close()
|
|
// _, err = obj.Call(ctx, "Store", cfg) // this.xxx 在 obj 活着期间保持
|
|
//
|
|
// obj2, err := queryCtrl.New(ctx) // 同一个 ctx → 同一份 store
|
|
// defer obj2.Close()
|
|
//
|
|
// 每个实例独占一个 VM,所以:
|
|
//
|
|
// - 不同请求的实例互不相干,完全并行
|
|
// - 同一个 ctx 下的不同 controller 各有各的 VM,也是并行的,只是共享扩展
|
|
// - 同一个实例的多次调用串行——这是保住 this.xxx 必需的
|
|
//
|
|
// 共享数据走扩展(Go 侧对象,注入到各个 VM 的是同一份引用),而不是共用 Runtime:
|
|
// 共用 Runtime 会让所有脚本被迫串行,那才是真的并发瓶颈。
|
|
//
|
|
// 设备连接这类长期存在的东西,业务侧自己拿 map 存 Instance 就行,跟 Go 版 controller 的
|
|
// 写法一致;本库不代管这个生命周期。
|
|
|
|
type scopeCtxKey struct{}
|
|
|
|
// scope 是一个作用域:一个标识、一批专属的全局对象、若干扩展。
|
|
// 它整个装在 ctx 里传递,不持有任何 VM,也不管实例的生命周期。
|
|
type scope struct {
|
|
key string
|
|
globals map[string]any
|
|
exts []Extension
|
|
}
|
|
|
|
// ScopeOption 配置 WithScope 装进 ctx 的作用域。
|
|
type ScopeOption func(*scope)
|
|
|
|
// ScopeKey 给作用域一个标识,脚本里可以通过 scope.key 读到,也会出现在日志里。
|
|
// 不给就生成一个随机的。它只是个标识,不参与任何查找或复用。
|
|
func ScopeKey(key string) ScopeOption {
|
|
return func(s *scope) { s.key = key }
|
|
}
|
|
|
|
// ScopeExtensions 把扩展带进这个作用域,它们会成为脚本里的全局对象。
|
|
//
|
|
// 实例由你自己创建,所以 Go 侧和脚本读写的天然是同一份,不用再取回来:
|
|
//
|
|
// st := jscriptx.NewStore()
|
|
// ctx = jscriptx.WithScope(ctx, jscriptx.ScopeExtensions(st))
|
|
// // …脚本里 store.Set("k", v)…
|
|
// st.Get("k")
|
|
//
|
|
// 同名的后者覆盖前者;跟白名单(WithGlobals)同名时以白名单为准。
|
|
func ScopeExtensions(exts ...Extension) ScopeOption {
|
|
return func(s *scope) { s.exts = append(s.exts, exts...) }
|
|
}
|
|
|
|
// ScopeGlobals 给这个作用域注入专属的全局对象,同名时盖过扩展。
|
|
// 这个 ctx 下 New 出来的实例都能看到它,适合放当前用户、当前设备这类东西。
|
|
//
|
|
// 注意它是**建 VM 时**注入的,也就是 New 的那一刻。实例建好之后再改 ctx 不会影响它,
|
|
// 每次调用都可能变的东西当调用参数传。
|
|
func ScopeGlobals(globals map[string]any) ScopeOption {
|
|
return func(s *scope) {
|
|
if s.globals == nil {
|
|
s.globals = map[string]any{}
|
|
}
|
|
for k, v := range globals {
|
|
s.globals[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithScope 把一个作用域装进 ctx。没给 ScopeKey 时生成一个随机标识。
|
|
func WithScope(ctx context.Context, opts ...ScopeOption) context.Context {
|
|
sc := &scope{}
|
|
for _, opt := range opts {
|
|
opt(sc)
|
|
}
|
|
if sc.key == "" {
|
|
sc.key = randomKey()
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return context.WithValue(ctx, scopeCtxKey{}, sc)
|
|
}
|
|
|
|
// ScopeKeyOf 取回 ctx 里的作用域标识,没有作用域时第二个返回值是 false。
|
|
func ScopeKeyOf(ctx context.Context) (string, bool) {
|
|
sc, ok := scopeOf(ctx)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return sc.key, true
|
|
}
|
|
|
|
// ScopeExtensionOf 按名字取回 ctx 里的扩展,没有时返回 nil。
|
|
//
|
|
// 通常用不上——扩展实例是你自己创建的,直接拿着那个变量就行。这个函数是给
|
|
// 「ctx 从别处传来、手上没有原对象」的场合准备的。
|
|
func ScopeExtensionOf(ctx context.Context, name string) Extension {
|
|
sc, ok := scopeOf(ctx)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
// 倒着找,同名时后注册的生效
|
|
for i := len(sc.exts) - 1; i >= 0; i-- {
|
|
if sc.exts[i].Name() == name {
|
|
return sc.exts[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scopeOf(ctx context.Context) (*scope, bool) {
|
|
if ctx == nil {
|
|
return nil, false
|
|
}
|
|
sc, ok := ctx.Value(scopeCtxKey{}).(*scope)
|
|
return sc, ok
|
|
}
|
|
|
|
// vmGlobals 拼出这个作用域下每个 VM 都要注入的全局:各个扩展的方法集、
|
|
// 调用方给的 ScopeGlobals,加上一个描述作用域身份的 scope 对象。
|
|
//
|
|
// 注入到各个 VM 里的扩展是同一个 Go 对象,所以同一个 ctx 下的脚本天然共享数据,
|
|
// 又不必共用 Runtime。
|
|
func (sc *scope) vmGlobals() map[string]any {
|
|
g := make(map[string]any, len(sc.globals)+len(sc.exts)+1)
|
|
for _, ext := range sc.exts {
|
|
g[ext.Name()] = ext.Bindings()
|
|
}
|
|
// ScopeGlobals 优先级高于扩展
|
|
for k, v := range sc.globals {
|
|
g[k] = v
|
|
}
|
|
g["scope"] = map[string]any{"key": sc.key}
|
|
return g
|
|
}
|
|
|
|
func randomKey() string {
|
|
var b [12]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
// crypto/rand 出问题时退回时间戳,重复的概率仍然极低
|
|
return "t" + hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano)))
|
|
}
|
|
return hex.EncodeToString(b[:])
|
|
}
|