约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.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 是**共享同一个
对象**的——扩展走的正是这条路,不该被当成隔离保证
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package jscriptx
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
// DefaultFunc 传给 Call/CallInto/WithCall 的 fn 参数时,表示脚本的默认导出本身,
|
|
// 也就是 `export default function ...` 这种"整个脚本就是一个函数"的写法。
|
|
// 默认导出是 class 或对象时用不上它——那种要按方法名调用。
|
|
const DefaultFunc = ""
|
|
|
|
// Script 是一份编译好的脚本,内部维护一个 VM 池。它并发安全,可以长期持有。
|
|
//
|
|
// 一个 Script 对应一份不可变的编译产物;热更新时 Engine 会造一个新的 Script
|
|
// 顶替它,已经拿着旧 Script 的调用不受影响,会继续跑完旧版本。
|
|
type Script struct {
|
|
engine *Engine
|
|
name string
|
|
version string
|
|
prog *goja.Program
|
|
|
|
pool chan *vmHandle
|
|
closed atomic.Bool
|
|
|
|
created atomic.Int64 // 累计新建过多少个 VM
|
|
dropped atomic.Int64 // 累计丢弃过多少个 VM(超时/panic/池满)
|
|
}
|
|
|
|
// vmHandle 是一个 VM:一个 goja.Runtime 加上它跑完脚本后的求值结果。
|
|
// Runtime 不是并发安全的,同一时刻只能有一个 goroutine 持有它。
|
|
//
|
|
// 一个 VM 只跑一个脚本。同一作用域下多个脚本要共享数据时靠扩展(Go 侧对象,注入到
|
|
// 各个 VM 里的是同一份引用),而不是共用 Runtime——共用 Runtime 会让作用域内的所有
|
|
// 脚本被迫串行。
|
|
type vmHandle struct {
|
|
rt *goja.Runtime
|
|
defFn goja.Value // 脚本求值出的函数(单函数入口写法)
|
|
exports *goja.Object // 脚本求值出的对象(class 实例写法),方法调用时绑定为 this
|
|
ctor *goja.Object // 导出的 class 本身。静态方法挂在它上面,实例的原型链上没有
|
|
|
|
// scoped 表示这个 VM 注入过某个作用域的扩展,因此**不能**回池给别的作用域用。
|
|
// 用完直接丢,见 release。
|
|
scoped bool
|
|
}
|
|
|
|
// Name 返回脚本名。
|
|
func (s *Script) Name() string { return s.name }
|
|
|
|
// Version 返回脚本版本号(Loader 给的,或者源码哈希)。
|
|
func (s *Script) Version() string { return s.version }
|
|
|
|
// Stats 是 VM 池的运行统计,用于观测。
|
|
type Stats struct {
|
|
Pooled int // 池里闲置的 VM 数
|
|
MaxVMs int // 池容量
|
|
Created int64 // 累计新建
|
|
Dropped int64 // 累计丢弃
|
|
}
|
|
|
|
// Stats 返回 VM 池的当前统计。
|
|
func (s *Script) Stats() Stats {
|
|
return Stats{
|
|
Pooled: len(s.pool),
|
|
MaxVMs: cap(s.pool),
|
|
Created: s.created.Load(),
|
|
Dropped: s.dropped.Load(),
|
|
}
|
|
}
|
|
|
|
// Has 判断脚本导出的实例上有没有这个方法(继承来的也算)。
|
|
func (s *Script) Has(fn string) bool {
|
|
inst, err := s.borrow(context.Background())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer s.release(inst, true)
|
|
_, _, _, ok := inst.lookup(fn)
|
|
return ok
|
|
}
|
|
|
|
// Close 释放池里所有 VM。之后的调用会返回 ErrClosed。
|
|
// 已经借出去、正在执行的调用不受影响,它们的 VM 归还时直接丢弃。
|
|
//
|
|
// New 出来的实例不在这条链上——它们的生命周期归调用方,Close 之后那些实例的调用
|
|
// 会因为脚本已关闭而报错,VM 等 GC 回收。
|
|
func (s *Script) Close() {
|
|
if s.closed.Swap(true) {
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case <-s.pool:
|
|
s.dropped.Add(1)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|