package jscriptx import ( "context" "crypto/rand" "encoding/hex" "time" ) // 作用域通过 context 传递:它只携带一段业务流程里要共享的东西(扩展和全局对象), // 不管任何生命周期——实例的生死由你 Close 决定。 // // st := jscriptx.NewStore() // ctx = jscriptx.WithScope(ctx, jscriptx.ScopeExtensions(st)) // // obj, err := resCtrl.New(ctx, arg) // 从 ctx 拿扩展和全局 // defer obj.Close() // _, err = obj.Call(ctx, "Store", cfg) // this.xxx 在 obj 活着期间保持 // // obj2, err := queryCtrl.New(ctx) // 同一个 ctx → 同一份 store // defer obj2.Close() // // 每个实例独占一个 VM,所以: // // - 不同请求的实例互不相干,完全并行 // - 同一个 ctx 下的不同 controller 各有各的 VM,也是并行的,只是共享扩展 // - 同一个实例的多次调用串行——这是保住 this.xxx 必需的 // // 共享数据走扩展(Go 侧对象,注入到各个 VM 的是同一份引用),而不是共用 Runtime: // 共用 Runtime 会让所有脚本被迫串行,那才是真的并发瓶颈。 // // 设备连接这类长期会话,业务侧自己拿 map 存 Instance 就行,跟 Go 版 controller 的 // 写法一致;本库不代管这个生命周期。 type scopeCtxKey struct{} // scope 是一个作用域:一个标识、一批专属的全局对象、若干扩展。 // 它整个装在 ctx 里传递,不持有任何 VM,也不管实例的生命周期。 type scope struct { key string globals map[string]any exts []Extension } // ScopeOption 配置 WithScope 装进 ctx 的作用域。 type ScopeOption func(*scope) // ScopeKey 给作用域一个标识,脚本里可以通过 scope.key 读到,也会出现在日志里。 // 不给就生成一个随机的。它只是个标识,不参与任何查找或复用。 func ScopeKey(key string) ScopeOption { return func(s *scope) { s.key = key } } // ScopeExtensions 把扩展带进这个作用域,它们会成为脚本里的全局对象。 // // 实例由你自己创建,所以 Go 侧和脚本读写的天然是同一份,不用再取回来: // // st := jscriptx.NewStore() // ctx = jscriptx.WithScope(ctx, jscriptx.ScopeExtensions(st)) // // …脚本里 store.Set("k", v)… // st.Get("k") // // 同名的后者覆盖前者;跟白名单(WithGlobals)同名时以白名单为准。 func ScopeExtensions(exts ...Extension) ScopeOption { return func(s *scope) { s.exts = append(s.exts, exts...) } } // ScopeGlobals 给这个作用域注入专属的全局对象,同名时盖过扩展。 // 这个 ctx 下 New 出来的实例都能看到它,适合放当前用户、当前设备这类东西。 // // 注意它是**建 VM 时**注入的,也就是 New 的那一刻。实例建好之后再改 ctx 不会影响它, // 每次调用都可能变的东西当调用参数传。 func ScopeGlobals(globals map[string]any) ScopeOption { return func(s *scope) { if s.globals == nil { s.globals = map[string]any{} } for k, v := range globals { s.globals[k] = v } } } // WithScope 把一个作用域装进 ctx。没给 ScopeKey 时生成一个随机标识。 func WithScope(ctx context.Context, opts ...ScopeOption) context.Context { sc := &scope{} for _, opt := range opts { opt(sc) } if sc.key == "" { sc.key = randomKey() } if ctx == nil { ctx = context.Background() } return context.WithValue(ctx, scopeCtxKey{}, sc) } // ScopeKeyOf 取回 ctx 里的作用域标识,没有作用域时第二个返回值是 false。 func ScopeKeyOf(ctx context.Context) (string, bool) { sc, ok := scopeOf(ctx) if !ok { return "", false } return sc.key, true } // ScopeExtensionOf 按名字取回 ctx 里的扩展,没有时返回 nil。 // // 通常用不上——扩展实例是你自己创建的,直接拿着那个变量就行。这个函数是给 // 「ctx 从别处传来、手上没有原对象」的场合准备的。 func ScopeExtensionOf(ctx context.Context, name string) Extension { sc, ok := scopeOf(ctx) if !ok { return nil } // 倒着找,同名时后注册的生效 for i := len(sc.exts) - 1; i >= 0; i-- { if sc.exts[i].Name() == name { return sc.exts[i] } } return nil } func scopeOf(ctx context.Context) (*scope, bool) { if ctx == nil { return nil, false } sc, ok := ctx.Value(scopeCtxKey{}).(*scope) return sc, ok } // vmGlobals 拼出这个作用域下每个 VM 都要注入的全局:各个扩展的方法集、 // 调用方给的 ScopeGlobals,加上一个描述作用域身份的 scope 对象。 // // 注入到各个 VM 里的扩展是同一个 Go 对象,所以同一个 ctx 下的脚本天然共享数据, // 又不必共用 Runtime。 func (sc *scope) vmGlobals() map[string]any { g := make(map[string]any, len(sc.globals)+len(sc.exts)+1) for _, ext := range sc.exts { g[ext.Name()] = ext.Bindings() } // ScopeGlobals 优先级高于扩展 for k, v := range sc.globals { g[k] = v } g["scope"] = map[string]any{"key": sc.key} return g } func randomKey() string { var b [12]byte if _, err := rand.Read(b[:]); err != nil { // crypto/rand 出问题时退回时间戳,重复的概率仍然极低 return "t" + hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano))) } return hex.EncodeToString(b[:]) }