Files
jscriptx/static.go
T
what 111665d2e7 fix: HasStatic 让 Stats 越用越偏;borrowStatic 漏标 scoped
两个都是静态调用那条路上的。

HasStatic 建了 VM 却不记丢弃。Created 在 load 里加(所有建 VM 的路径都走它),
Dropped 在 staticTarget.finish 里加,而 HasStatic 拿到 VM 后直接返回,不走
finish。每调一次 Created-Dropped 就永久 +1——Stats 是导出的观测结构,这个
偏差会直接体现在监控上。改成走同一套记账。

borrowStatic 同样会注入作用域扩展,却不设 vm.scoped。release 靠这个标志拒绝
把带扩展的 VM 放回池子,漏标就是跨调用泄漏。今天不出事只是因为
staticTarget.finish 永远丢弃——同一个约束靠两套机制守,以后有人把静态 VM
接进 release 就会漏。

测试:
  - Stats 那条断言「在飞的 VM 数」调用前后不变
  - scoped 那条是**内部测试**,直接断言标志位。行为测试盖不住它——borrowStatic
    那条路今天靠 finish 兜底,从外面看不出漏标
  两条都做了破坏性验证,摘掉修复会红。

顺带把 WithTimeout 的文档补上一句:要新建 VM 的路径上是顶层和函数调用两段
各自计时,墙钟上限是 2×d。行为不改——两段各自计时是合理的,缺的只是说明。
2026-09-10 15:21:17 +08:00

102 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jscriptx
import "context"
// CallStatic 调用脚本导出的 class 上的**静态方法**。
//
// export default class PkgImportController {
// static Startup() { store.Set("pkg.registry", "https://…") }
// Execute(g) { … }
// }
//
// _, err := script.CallStatic(ctx, "Startup")
//
// 它跟 Call 有两点不同,都是"静态"这个语义要求的:
//
// - **不构造实例**constructor 不会跑。构造函数做的是"这一次调用的准备",
// 跟脚本级的初始化无关,跑它只会白费一遍还可能有副作用。
// - **VM 用完就丢**,不回池。静态方法一般只在脚本生命周期里跑一两次,
// 为它留一个 VM 不划算;而且它做的事通常是往 Go 侧扩展里写东西,
// 那些副作用留在 Go 那边,VM 本身没有保留的价值。
//
// 脚本没写这个静态方法时返回的错误可以被 errors.Is(err, ErrFuncNotFound) 匹配上
// ——生命周期钩子多半是可选的,用它来区分"没写"和"写了但炸了"
//
// if _, err := script.CallStatic(ctx, "Startup"); err != nil &&
// !errors.Is(err, ErrFuncNotFound) {
// return err // 真的出错了
// }
//
// 静态方法里能用 ctx 带进来的作用域扩展(WithScope),这正是它的用武之地:
// 把配置写进 store、往 Go 侧注册东西。但**它建的 JS 对象活不下来**——
// VM 一丢就没了,别指望后续调用能看到。
func (s *Script) CallStatic(ctx context.Context, fn string, args ...any) (any, error) {
if fn == DefaultFunc {
return nil, newError(KindNotFound, s.name, fn, ErrFuncNotFound,
"CallStatic 要给一个静态方法名")
}
return callAny(ctx, staticTarget{s}, fn, args)
}
// HasStatic 判断脚本导出的 class 上有没有这个静态方法。
//
// 它要建一个 VM 才能回答,所以别在热路径上反复调;只想"有就调"的话,
// 直接 CallStatic 然后判 ErrFuncNotFound 更省。
func (s *Script) HasStatic(fn string) bool {
if fn == DefaultFunc {
return false
}
vm, err := s.borrowStatic(context.Background())
if err != nil {
return false
}
// 建了 VM 就得记一笔丢弃,走跟 CallStatic 同一套记账。漏掉的话每调一次
// Stats().Created - Dropped 就永久 +1
defer staticTarget{s}.finish(vm, true)
// ctor != nil 排除 lookup 的全局兜底分支:静态方法必须挂在 class 上,
// 脚本导出的不是 class 时 lookup 可能在全局里找到同名函数
_, _, _, ok := vm.lookup(fn)
return ok && vm.ctor != nil
}
// staticTarget 让静态调用复用 invoke 那一整套(超时中断、panic 恢复、错误分类),
// 只是换一种取 VM 的方式:不构造实例,用完丢弃。
type staticTarget struct{ s *Script }
func (t staticTarget) owner() *Script { return t.s }
func (t staticTarget) acquire(ctx context.Context) (*vmHandle, error) {
return t.s.borrowStatic(ctx)
}
// finish 直接丢弃,不回池——见 CallStatic 的说明。
func (t staticTarget) finish(*vmHandle, bool) { t.s.dropped.Add(1) }
// borrowStatic 造一个只装了 class、没有实例的 VM。
func (s *Script) borrowStatic(ctx context.Context) (*vmHandle, error) {
if s.closed.Load() {
return nil, newError(KindClosed, s.name, "", ErrClosed, "脚本已关闭")
}
var extra map[string]any
if sc, ok := scopeOf(ctx); ok {
extra = sc.vmGlobals()
}
rt, err := s.engine.newRuntime(s.name, extra)
if err != nil {
return nil, err
}
vm, err := s.load(ctx, rt, nil, true)
if err != nil {
return nil, err
}
// 带作用域的 VM 装着那个作用域的扩展,绝不能回池。今天 staticTarget.finish
// 永远丢弃,标不标都不漏;但同一个约束不该靠两套机制守——以后有人把静态 VM
// 接进 release,漏标就是一次跨请求泄漏
vm.scoped = extra != nil
return vm, nil
}