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 都仍能编译。
236 lines
7.8 KiB
Go
236 lines
7.8 KiB
Go
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)
|