Files
jscriptx/script.go
T
what 7e1893b246 refactor: 文件按主类型划分,一个 struct 一个功能
约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.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 是**共享同一个
                 对象**的——扩展走的正是这条路,不该被当成隔离保证
2026-09-10 15:28:59 +08:00

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
}
}
}