Files
what 9b3509ae29 refactor: 清掉指向不存在的 Dispatch 的文档,删掉为它留的死导出
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 都仍能编译。
2026-09-10 15:15:10 +08:00

236 lines
7.8 KiB
Go
Raw Permalink 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 (
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
// VersionSep 分隔脚本名和版本标签。叠层时用 "脚本名@版本" 点名要哪一层的实现。
const VersionSep = "@"
// OverlayLoader 把若干个 Loader 叠成一个,**后面的盖前面的**。
// 由 Overlay 创建。
type OverlayLoader struct {
loaders []Loader
prepared bool // 所有成员一致,构造时已校验过
byVer map[string]int // 版本标签 -> 层号,贴了标签的层才在里面
}
// Versioned 由带版本标签的 Loader 实现(比如 esm.Loader 配 WithVersion)。
// 叠层时这个标签就是调用点用来点名的那个:Load("Foo/Bar@v1")。
//
// 没实现这个接口、或者标签是空串的层,只能通过"上层盖下层"取到,点不了名。
type Versioned interface {
// Version 返回这一层的版本标签。
Version() string
}
// Tag 给任意 Loader 贴一个版本标签,让它在叠层里能被点名。
// 自带标签的 Loader(比如 esm.NewLoader 配了 WithVersion)不用它。
//
// e, _ := jscriptx.New(jscriptx.WithLoader(
// disk,
// jscriptx.Tag("hotfix", dbLoader), // 之后可以 Load("Foo/Bar@hotfix")
// ))
func Tag(version string, l Loader) Loader {
return &taggedLoader{Loader: l, version: version}
}
type taggedLoader struct {
Loader
version string
}
func (t *taggedLoader) Version() string { return t.version }
// Prepared 透传底下那个 Loader 的取值,别让贴标签这件事改了它的性质。
func (t *taggedLoader) Prepared() bool { return isPrepared(t.Loader) }
func versionOf(l Loader) string {
v, ok := l.(Versioned)
if !ok {
return ""
}
return v.Version()
}
// Overlay 把多个 Loader 叠成一个:取脚本时从最后一个往前找,谁先有就用谁的。
// 排在后面的因此能覆盖前面的同名脚本——把"定制层"放最后,业务侧放一份同名脚本
// 就能改写默认实现,不用动被覆盖的那一份:
//
// base, _ := esm.NewLoader("app/src")
// custom, _ := esm.NewLoader("custom/src") // 配置可以跟 base 完全不同
//
// loader, err := jscriptx.Overlay(base, custom) // custom 盖 base
// e, _ := jscriptx.New(jscriptx.WithLoader(loader))
//
// 每个成员是独立的 Loader,各有各的配置(入口规则、目标版本、node_modules 位置、
// 扩展模块),来源也可以不同——一层来自磁盘目录,另一层来自数据库都行。
//
// 给某一层贴了版本标签(esm 的 WithVersion,或者 Tag),调用点就能点名要它:
//
// v1, _ := esm.NewLoader("app/v1/src", esm.WithVersion("v1"))
// v2, _ := esm.NewLoader("app/v2/src", esm.WithVersion("v2"))
//
// e.New(ctx, "Resource/ResCreateController") // 不点名:上层盖下层,拿到 v2
// e.New(ctx, "Resource/ResCreateController@v1") // 点名:只在 v1 那层找,不回落
//
// 点名是"只认这一层":那层没有这个脚本就直接报不存在,不会掉到别的层去——
// 不然点名要 v1 却跑了 v2 的实现,比报错难查得多。
//
// 成员的 Prepared 必须一致:要么都是自己打好包的(比如 jscriptx/esm 的 Loader),
// 要么都交出原始源码由引擎打包。混着来会返回错误,因为引擎只能对整个 Loader
// 做一次判断,没法分脚本区别对待。真要混,把原始源码那层用 LoaderFunc 包一下,
// 里面自己调 Bundle,它就跟其它层一样是"打好包的"了。
func Overlay(loaders ...Loader) (*OverlayLoader, error) {
if len(loaders) == 0 {
return nil, errors.New("jscriptx: Overlay 至少要给一个 Loader")
}
for i, l := range loaders {
if l == nil {
return nil, fmt.Errorf("jscriptx: Overlay 的第 %d 个 Loader 是 nil", i)
}
}
prepared := isPrepared(loaders[0])
for i, l := range loaders[1:] {
if isPrepared(l) != prepared {
return nil, fmt.Errorf(
"jscriptx: Overlay 的成员 Prepared 不一致(第 0 个是 %v,第 %d 个是 %v);"+
"要么都自己打包,要么都交出原始源码", prepared, i+1, !prepared)
}
}
byVer := map[string]int{}
for i, l := range loaders {
v := versionOf(l)
if v == "" {
continue
}
if strings.Contains(v, VersionSep) {
return nil, fmt.Errorf("jscriptx: 版本标签 %q 里不能有 %q", v, VersionSep)
}
if j, dup := byVer[v]; dup {
return nil, fmt.Errorf("jscriptx: 版本标签 %q 重了(第 %d 层和第 %d 层)", v, j, i)
}
byVer[v] = i
}
return &OverlayLoader{
loaders: append([]Loader(nil), loaders...),
prepared: prepared,
byVer: byVer,
}, nil
}
// Load 从最后一个成员往前找,返回第一个找到的脚本。
//
// 成员报"脚本不存在"就继续往前找;报别的错直接返回——加载出故障不该被
// 后面那层的结果悄悄盖掉。
func (o *OverlayLoader) Load(name string) (string, string, error) {
if bare, version, ok := splitVersion(name); ok {
return o.loadFrom(bare, version)
}
var notFound error
for i := len(o.loaders) - 1; i >= 0; i-- {
source, version, err := o.loaders[i].Load(name)
if err != nil {
if errors.Is(err, ErrScriptNotFound) {
notFound = err
continue
}
return "", "", err
}
// 版本号带上是第几层给的:覆盖层的脚本删掉后会落回下面那层,
// 两层的版本号万一撞上,不带层号就看不出脚本已经换了人。
if version != "" {
version = strconv.Itoa(i) + ":" + version
}
return source, version, nil
}
if notFound == nil {
notFound = ErrScriptNotFound
}
return "", "", fmt.Errorf("%w: %s%d 层都没有)", notFound, name, len(o.loaders))
}
// loadFrom 只在点名的那一层找,找不到就报不存在,不回落到别的层。
func (o *OverlayLoader) loadFrom(name, version string) (string, string, error) {
i, ok := o.byVer[version]
if !ok {
return "", "", fmt.Errorf("%w: %s(没有版本 %q 这一层,有的是 %v)",
ErrScriptNotFound, name, version, o.Versions())
}
source, ver, err := o.loaders[i].Load(name)
if err != nil {
return "", "", err
}
if ver != "" {
ver = strconv.Itoa(i) + ":" + ver
}
return source, ver, nil
}
// splitVersion 把 "Foo/Bar@v1" 拆成 "Foo/Bar" 和 "v1"。
// 用最后一个分隔符,脚本名里真带了 @ 也不会拆错。
func splitVersion(name string) (bare, version string, ok bool) {
i := strings.LastIndex(name, VersionSep)
if i <= 0 || i == len(name)-1 {
return name, "", false // 没有分隔符,或者两边空着
}
return name[:i], name[i+1:], true
}
// Versions 返回各层的版本标签,按叠放顺序,没贴标签的层跳过。
func (o *OverlayLoader) Versions() []string {
var out []string
for _, l := range o.loaders {
if v := versionOf(l); v != "" {
out = append(out, v)
}
}
return out
}
// Prepared 返回成员们一致的取值,见 Overlay 的说明。
func (o *OverlayLoader) Prepared() bool { return o.prepared }
// Names 汇总所有成员的脚本名,去重后按字典序排列。
// 成员得有 Names() []string 方法才算得上,没有的(比如数据库来源)就跳过。
func (o *OverlayLoader) Names() []string {
seen := map[string]bool{}
var out []string
for _, l := range o.loaders {
lister, ok := l.(interface{ Names() []string })
if !ok {
continue
}
for _, n := range lister.Names() {
if !seen[n] {
seen[n] = true
out = append(out, n)
}
}
}
sort.Strings(out)
return out
}
// Rebuild 挨个让成员重建。成员没有 Rebuild() error 方法就跳过。
// 有成员失败时其余的照样会走一遍,返回的错误里带上所有失败。
func (o *OverlayLoader) Rebuild() error {
var errs []error
for i, l := range o.loaders {
r, ok := l.(interface{ Rebuild() error })
if !ok {
continue
}
if err := r.Rebuild(); err != nil {
errs = append(errs, fmt.Errorf("第 %d 层: %w", i, err))
}
}
return errors.Join(errs...)
}
var _ Prepared = (*OverlayLoader)(nil)