feat(js): support multi-instance pools, graceful shutdown and improved DTS generation
This commit is contained in:
parent
08d95ac2f2
commit
a2089b0c17
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@ -0,0 +1,14 @@
|
||||
# CHANGELOG - go/js
|
||||
|
||||
## v1.5.1 (2026-06-05)
|
||||
- **架构重构: 多例支持与优雅停机**:
|
||||
- 引入 `Pool` 结构体,支持通过 `js.NewPool()` 创建相互隔离的执行环境,避免业务间脚本冲突。
|
||||
- 实现 `starter.Service` 接口(Start, Stop, Status),支持集成到基础设施生命周期管理中。
|
||||
- 优化优雅停机逻辑:`Stop` 会自动阻塞并等待所有活跃的 JS 调用完成,或直到 Context 超时。
|
||||
- **文档增强: AI/IDE 丝滑对齐**:
|
||||
- 彻底重写 `js.Doc()`,采用 `declare const go` 全局声明,支持 VSCode 零配置代码提示。
|
||||
- **穿透防护**: 自动识别并拦截非项目路径下的结构体,统一映射为 `GoPkg_Name` 不透明句柄。
|
||||
- **方法提纯**: 自动过滤涉及 `io.Reader/Writer`、`reflect`、`sync` 等 JS 无法处理的底层方法。
|
||||
- **放行 time.Time**: 开放时间对象的方法反射,支持 JS 直接调用 `Unix()`, `Format()` 等业务方法。
|
||||
- **隐式类型导出**: 支持 `__export` 命名前缀,用于导出类型结构而不暴露工厂函数。
|
||||
- **稳定性**: 修复了基于原始类型别名(如 `time.Month`)导致反射崩溃的问题。
|
||||
286
doc.go
286
doc.go
@ -2,6 +2,7 @@ package js
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -9,23 +10,45 @@ import (
|
||||
"apigo.cc/go/jsmod"
|
||||
)
|
||||
|
||||
// Doc generates a TypeScript definition (.d.ts) for all registered Go modules.
|
||||
// This is designed to be fed into an AI (LLM) to provide context for low-code development.
|
||||
// Doc generates a comprehensive TypeScript definition (.d.ts) for the Go/JS environment.
|
||||
func Doc() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("// TypeScript Definitions for Go/JS Low-Code Environment\n\n")
|
||||
sb.WriteString("/**\n * Go/JS Low-Code Environment Type Definitions\n")
|
||||
sb.WriteString(" * Generated by js.Doc(). DO NOT EDIT.\n */\n\n")
|
||||
|
||||
// 1. Basic Opaque types (Manual fallback for critical non-project types)
|
||||
sb.WriteString("/** Opaque handle to Go context.Context */\n")
|
||||
sb.WriteString("interface GoContext { _isGoContext: true; }\n")
|
||||
sb.WriteString("/** Opaque handle to Go log.Logger */\n")
|
||||
sb.WriteString("interface GoLogger { _isGoLogger: true; }\n")
|
||||
sb.WriteString("/** Opaque handle to Go net/http.Request */\n")
|
||||
sb.WriteString("interface GoHttp_Request { _isGoHttpReq: true; }\n")
|
||||
sb.WriteString("/** Opaque handle to Go net/http.Response */\n")
|
||||
sb.WriteString("interface GoHttp_Response { _isGoHttpRes: true; }\n")
|
||||
sb.WriteString("/** Opaque handle to Go net/url.URL */\n")
|
||||
sb.WriteString("interface GoNet_URL { _isGoNetURL: true; }\n\n")
|
||||
|
||||
modules := jsmod.GetModules()
|
||||
keys := make([]string, 0, len(modules))
|
||||
modNames := make([]string, 0, len(modules))
|
||||
for k := range modules {
|
||||
keys = append(keys, k)
|
||||
modNames = append(modNames, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
sort.Strings(modNames)
|
||||
|
||||
sb.WriteString("declare namespace go {\n")
|
||||
for _, modName := range keys {
|
||||
ctx := &docCtx{
|
||||
seenTypes: make(map[reflect.Type]string),
|
||||
interfaces: make(map[string]string),
|
||||
opaqueList: make(map[string]bool),
|
||||
}
|
||||
|
||||
// 2. Build Module Interfaces
|
||||
moduleDefs := make(map[string]string)
|
||||
for _, modName := range modNames {
|
||||
mod := modules[modName]
|
||||
sb.WriteString(fmt.Sprintf(" namespace %s {\n", modName))
|
||||
ctx.currentMod = modName
|
||||
|
||||
var msb strings.Builder
|
||||
msb.WriteString(fmt.Sprintf("interface %s_Module {\n", strings.Title(modName)))
|
||||
|
||||
expKeys := make([]string, 0, len(mod.Exports))
|
||||
for k := range mod.Exports {
|
||||
@ -34,57 +57,112 @@ func Doc() string {
|
||||
sort.Strings(expKeys)
|
||||
|
||||
for _, name := range expKeys {
|
||||
isHidden := strings.HasPrefix(name, "__export")
|
||||
val := mod.Exports[name]
|
||||
isUnsafe := mod.UnsafeList[name]
|
||||
if isUnsafe {
|
||||
sb.WriteString(" /** @unsafe */\n")
|
||||
|
||||
memberDef := formatExport(name, val, ctx, false)
|
||||
|
||||
if !isHidden {
|
||||
isUnsafe := mod.UnsafeList[name]
|
||||
if isUnsafe {
|
||||
msb.WriteString(" /** @unsafe */\n")
|
||||
}
|
||||
msb.WriteString(fmt.Sprintf(" %s\n", memberDef))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s\n", formatExport(name, val)))
|
||||
}
|
||||
sb.WriteString(" }\n")
|
||||
msb.WriteString("}")
|
||||
moduleDefs[modName] = msb.String()
|
||||
}
|
||||
sb.WriteString("}\n")
|
||||
|
||||
// 2. Output Opaque Interfaces (Dynamic)
|
||||
opaqueNames := make([]string, 0, len(ctx.opaqueList))
|
||||
for name := range ctx.opaqueList {
|
||||
opaqueNames = append(opaqueNames, name)
|
||||
}
|
||||
sort.Strings(opaqueNames)
|
||||
for _, name := range opaqueNames {
|
||||
sb.WriteString(fmt.Sprintf("interface %s { _is%s: true; }\n", name, name))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// 3. Supporting Interfaces
|
||||
if len(ctx.interfaces) > 0 {
|
||||
sb.WriteString("// --- Supporting Interfaces ---\n\n")
|
||||
ifaceNames := make([]string, 0, len(ctx.interfaces))
|
||||
for name := range ctx.interfaces {
|
||||
ifaceNames = append(ifaceNames, name)
|
||||
}
|
||||
sort.Strings(ifaceNames)
|
||||
for _, name := range ifaceNames {
|
||||
sb.WriteString(ctx.interfaces[name])
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Module Interfaces
|
||||
sb.WriteString("// --- Module Definitions ---\n\n")
|
||||
for _, modName := range modNames {
|
||||
sb.WriteString(moduleDefs[modName])
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 5. Global 'go' declaration
|
||||
sb.WriteString("/** Global entry point for Go bridged modules */\n")
|
||||
sb.WriteString("declare const go: {\n")
|
||||
for _, modName := range modNames {
|
||||
sb.WriteString(fmt.Sprintf(" readonly %s: %s_Module;\n", modName, strings.Title(modName)))
|
||||
}
|
||||
sb.WriteString("};\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatExport(name string, val any) string {
|
||||
type docCtx struct {
|
||||
currentMod string
|
||||
seenTypes map[reflect.Type]string
|
||||
interfaces map[string]string
|
||||
opaqueList map[string]bool
|
||||
}
|
||||
|
||||
func formatExport(name string, val any, ctx *docCtx, isMethod bool) string {
|
||||
t := reflect.TypeOf(val)
|
||||
if t == nil {
|
||||
return fmt.Sprintf("const %s: any;", name)
|
||||
return fmt.Sprintf("%s: any;", name)
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Func {
|
||||
return fmt.Sprintf("function %s%s;", name, formatFunc(t))
|
||||
return fmt.Sprintf("%s%s;", name, formatFunc(t, ctx, isMethod))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("const %s: %s;", name, goTypeToTS(t))
|
||||
return fmt.Sprintf("%s: %s;", name, goTypeToTS(t, ctx))
|
||||
}
|
||||
|
||||
func formatFunc(t reflect.Type) string {
|
||||
func formatFunc(t reflect.Type, ctx *docCtx, isMethod bool) string {
|
||||
var params []string
|
||||
numIn := t.NumIn()
|
||||
jsArgIdx := 0
|
||||
|
||||
for i := 0; i < numIn; i++ {
|
||||
startIdx := 0
|
||||
if isMethod {
|
||||
startIdx = 1 // Skip receiver
|
||||
}
|
||||
|
||||
for i := startIdx; i < numIn; i++ {
|
||||
argType := t.In(i)
|
||||
// Skip Context and Logger in TS doc
|
||||
typeName := argType.String()
|
||||
if typeName == "context.Context" || typeName == "*log.Logger" {
|
||||
if typeName == "context.Context" || strings.Contains(typeName, "log.Logger") {
|
||||
continue
|
||||
}
|
||||
|
||||
params = append(params, fmt.Sprintf("arg%d: %s", jsArgIdx, goTypeToTS(argType)))
|
||||
params = append(params, fmt.Sprintf("arg%d: %s", jsArgIdx, goTypeToTS(argType, ctx)))
|
||||
jsArgIdx++
|
||||
}
|
||||
|
||||
// Handle return values
|
||||
numOut := t.NumOut()
|
||||
var retType string
|
||||
if numOut == 0 {
|
||||
retType = "void"
|
||||
} else {
|
||||
// If last return is error, we only care about the first part for TS doc
|
||||
realOut := numOut
|
||||
if numOut > 0 && t.Out(numOut-1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||
realOut--
|
||||
@ -93,7 +171,7 @@ func formatFunc(t reflect.Type) string {
|
||||
if realOut <= 0 {
|
||||
retType = "void"
|
||||
} else if realOut == 1 {
|
||||
retType = goTypeToTS(t.Out(0))
|
||||
retType = goTypeToTS(t.Out(0), ctx)
|
||||
} else {
|
||||
retType = "any[]"
|
||||
}
|
||||
@ -102,43 +180,169 @@ func formatFunc(t reflect.Type) string {
|
||||
return fmt.Sprintf("(%s): %s", strings.Join(params, ", "), retType)
|
||||
}
|
||||
|
||||
func goTypeToTS(t reflect.Type) string {
|
||||
func goTypeToTS(t reflect.Type, ctx *docCtx) string {
|
||||
if t == nil {
|
||||
return "any"
|
||||
}
|
||||
|
||||
// Handle known standard library types to avoid deep recursion and provide clear naming
|
||||
typeName := t.String()
|
||||
switch typeName {
|
||||
case "time.Time", "*time.Time":
|
||||
return "time.Time"
|
||||
case "time.Duration", "*time.Duration":
|
||||
return "time.Duration"
|
||||
}
|
||||
|
||||
// Handle pointers
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
pkgPath := t.PkgPath()
|
||||
rawName := t.Name()
|
||||
|
||||
// 1. Blacklist / Filter
|
||||
if isTypeUnusable(t) {
|
||||
return "any"
|
||||
}
|
||||
|
||||
// 2. Map primitive-like types (even with PkgPath) to TS primitives
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
return "string"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
// time.Duration is an Int64, but we treat it as number (ms)
|
||||
return "number"
|
||||
case reflect.Bool:
|
||||
return "boolean"
|
||||
case reflect.Slice, reflect.Array:
|
||||
return goTypeToTS(t.Elem()) + "[]"
|
||||
return goTypeToTS(t.Elem(), ctx) + "[]"
|
||||
case reflect.Map:
|
||||
return "Record<string, any>"
|
||||
// http.Header as a special Record mapping
|
||||
if pkgPath == "net/http" && rawName == "Header" {
|
||||
return "Record<string, string[]>"
|
||||
}
|
||||
return fmt.Sprintf("Record<%s, %s>", goTypeToTS(t.Key(), ctx), goTypeToTS(t.Elem(), ctx))
|
||||
}
|
||||
|
||||
// 3. Special Mappings for remaining named types (mostly Structs/Interfaces)
|
||||
if pkgPath == "time" && rawName == "Time" {
|
||||
return registerInterface(t, ctx)
|
||||
}
|
||||
|
||||
// 4. Strict Penetration Blocking for named non-project types
|
||||
isProject := strings.HasPrefix(pkgPath, "apigo.cc/go/")
|
||||
isAnonymous := rawName == ""
|
||||
|
||||
if pkgPath != "" && !isProject {
|
||||
base := filepath.Base(pkgPath)
|
||||
opaqueName := "Go" + strings.Title(base) + "_" + rawName
|
||||
ctx.opaqueList[opaqueName] = true
|
||||
return opaqueName
|
||||
}
|
||||
|
||||
// 5. Recursive Struct/Interface Parsing
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
return "{ [key: string]: any }"
|
||||
if isAnonymous {
|
||||
return "{ [key: string]: any }"
|
||||
}
|
||||
return registerInterface(t, ctx)
|
||||
case reflect.Interface:
|
||||
return "any"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
func registerInterface(t reflect.Type, ctx *docCtx) string {
|
||||
if name, ok := ctx.seenTypes[t]; ok {
|
||||
return name
|
||||
}
|
||||
|
||||
rawName := t.Name()
|
||||
pkgPath := t.PkgPath()
|
||||
|
||||
var name string
|
||||
if strings.HasPrefix(pkgPath, "apigo.cc/go/") {
|
||||
parts := strings.Split(pkgPath, "/")
|
||||
name = fmt.Sprintf("%s_%s", strings.Title(parts[len(parts)-1]), rawName)
|
||||
} else if pkgPath != "" {
|
||||
// Standard lib types like time.Time
|
||||
base := filepath.Base(pkgPath)
|
||||
name = fmt.Sprintf("Go%s_%s", strings.Title(base), rawName)
|
||||
} else {
|
||||
name = fmt.Sprintf("%s_%s", strings.Title(ctx.currentMod), rawName)
|
||||
}
|
||||
|
||||
ctx.seenTypes[t] = name
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("interface %s {\n", name))
|
||||
|
||||
// Fields with Tag check
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.PkgPath != "" || field.Tag.Get("js") == "-" {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s: %s;\n", field.Name, goTypeToTS(field.Type, ctx)))
|
||||
}
|
||||
|
||||
// Methods check
|
||||
ptrType := reflect.PtrTo(t)
|
||||
methods := make(map[string]reflect.Method)
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
methods[m.Name] = m
|
||||
}
|
||||
for i := 0; i < ptrType.NumMethod(); i++ {
|
||||
m := ptrType.Method(i)
|
||||
methods[m.Name] = m
|
||||
}
|
||||
|
||||
mNames := make([]string, 0, len(methods))
|
||||
for n := range methods {
|
||||
mNames = append(mNames, n)
|
||||
}
|
||||
sort.Strings(mNames)
|
||||
|
||||
for _, n := range mNames {
|
||||
m := methods[n]
|
||||
if m.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
if isMethodUnusable(m.Type) {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s%s;\n", n, formatFunc(m.Type, ctx, true)))
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
ctx.interfaces[name] = sb.String()
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func isMethodUnusable(t reflect.Type) bool {
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
if isTypeUnusable(t.In(i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for i := 0; i < t.NumOut(); i++ {
|
||||
if isTypeUnusable(t.Out(i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTypeUnusable(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
pkgPath := t.PkgPath()
|
||||
rawName := t.Name()
|
||||
|
||||
if pkgPath == "reflect" || pkgPath == "runtime" || pkgPath == "unsafe" || pkgPath == "sync" {
|
||||
return true
|
||||
}
|
||||
if pkgPath == "io" && (rawName == "Reader" || rawName == "Writer" || rawName == "Closer") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
19
doc_test.go
19
doc_test.go
@ -15,18 +15,25 @@ func TestDocGeneration(t *testing.T) {
|
||||
return nil, nil
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"__exportInternal": func() *struct{ Name string } { return nil },
|
||||
})
|
||||
|
||||
doc := Doc()
|
||||
fmt.Println(doc)
|
||||
|
||||
if !strings.Contains(doc, "namespace db") {
|
||||
t.Error("doc should contain namespace db")
|
||||
if !strings.Contains(doc, "interface GoTime") {
|
||||
t.Error("doc should contain GoTime interface")
|
||||
}
|
||||
if !strings.Contains(doc, "function query(arg0: string, arg1: any[]): Record<string, any>[];") {
|
||||
t.Error("doc should contain query function with correct signature")
|
||||
if !strings.Contains(doc, "interface Db_Module") {
|
||||
t.Error("doc should contain Db_Module interface")
|
||||
}
|
||||
if !strings.Contains(doc, "const version: string;") {
|
||||
t.Error("doc should contain version constant")
|
||||
if !strings.Contains(doc, "declare const go:") {
|
||||
t.Error("doc should contain global go declaration")
|
||||
}
|
||||
if strings.Contains(doc, "__exportInternal") {
|
||||
t.Error("doc should NOT contain __exportInternal")
|
||||
}
|
||||
if !strings.Contains(doc, "interface Db_") {
|
||||
t.Error("doc should contain supporting interface for internal struct")
|
||||
}
|
||||
}
|
||||
|
||||
169
pool.go
169
pool.go
@ -3,11 +3,11 @@ package js
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"apigo.cc/go/jsmod"
|
||||
"apigo.cc/go/log"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
@ -16,12 +16,25 @@ type vmInstance struct {
|
||||
version int32
|
||||
}
|
||||
|
||||
var (
|
||||
globalVersion int32
|
||||
scripts []string
|
||||
scriptsMu sync.RWMutex
|
||||
// Pool represents an isolated JS execution environment with its own script registry and VM pool.
|
||||
type Pool struct {
|
||||
version int32
|
||||
scripts []string
|
||||
mu sync.RWMutex
|
||||
pool sync.Pool
|
||||
|
||||
pool = sync.Pool{
|
||||
// Lifecycle management
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
closed int32
|
||||
}
|
||||
|
||||
// NewPool creates a new isolated JS execution environment.
|
||||
func NewPool() *Pool {
|
||||
p := &Pool{}
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
p.pool = sync.Pool{
|
||||
New: func() any {
|
||||
return &vmInstance{
|
||||
runtime: createNewRuntime(),
|
||||
@ -29,7 +42,11 @@ var (
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
return p
|
||||
}
|
||||
|
||||
// DefaultPool is the global singleton pool.
|
||||
var DefaultPool = NewPool()
|
||||
|
||||
func createNewRuntime() *goja.Runtime {
|
||||
vm := goja.New()
|
||||
@ -55,54 +72,43 @@ func createNewRuntime() *goja.Runtime {
|
||||
return vm
|
||||
}
|
||||
|
||||
// Define adds JS code to the global registry and increments the version.
|
||||
// All VMs in the pool will eventually synchronize to this version.
|
||||
func Define(code string) {
|
||||
scriptsMu.Lock()
|
||||
defer scriptsMu.Unlock()
|
||||
// Define adds JS code to the pool's registry and increments the version.
|
||||
func (p *Pool) Define(code string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
scripts = append(scripts, code)
|
||||
atomic.AddInt32(&globalVersion, 1)
|
||||
}
|
||||
|
||||
// CallOption allows configuring the JS execution environment.
|
||||
type CallOption func(vm *goja.Runtime)
|
||||
|
||||
// WithSafeMode enables or disables safe mode for the call.
|
||||
func WithSafeMode(enabled bool) CallOption {
|
||||
return func(vm *goja.Runtime) {
|
||||
_ = vm.Set("__safeMode__", enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger injects a custom logger for the call.
|
||||
func WithLogger(logger *log.Logger) CallOption {
|
||||
return func(vm *goja.Runtime) {
|
||||
_ = vm.Set("__logger__", vm.ToValue(logger))
|
||||
}
|
||||
p.scripts = append(p.scripts, code)
|
||||
atomic.AddInt32(&p.version, 1)
|
||||
}
|
||||
|
||||
// Call executes a JS function from the pool.
|
||||
// It automatically synchronizes the VM to the latest version.
|
||||
func Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||
instance := pool.Get().(*vmInstance)
|
||||
defer pool.Put(instance)
|
||||
func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||
if atomic.LoadInt32(&p.closed) == 1 {
|
||||
return nil, fmt.Errorf("js.Pool: pool is closed")
|
||||
}
|
||||
|
||||
instance := p.pool.Get().(*vmInstance)
|
||||
defer p.pool.Put(instance)
|
||||
|
||||
// Tracking active calls for graceful shutdown
|
||||
p.wg.Add(1)
|
||||
defer p.wg.Done()
|
||||
|
||||
vm := instance.runtime
|
||||
|
||||
// 1. Synchronize scripts if version is behind
|
||||
currentGlobalVersion := atomic.LoadInt32(&globalVersion)
|
||||
if instance.version < currentGlobalVersion {
|
||||
scriptsMu.RLock()
|
||||
for i := int(instance.version); i < len(scripts); i++ {
|
||||
_, err := vm.RunString(scripts[i])
|
||||
currentVersion := atomic.LoadInt32(&p.version)
|
||||
if instance.version < currentVersion {
|
||||
p.mu.RLock()
|
||||
for i := int(instance.version); i < len(p.scripts); i++ {
|
||||
_, err := vm.RunString(p.scripts[i])
|
||||
if err != nil {
|
||||
scriptsMu.RUnlock()
|
||||
p.mu.RUnlock()
|
||||
return nil, fmt.Errorf("js.sync error at script %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
instance.version = currentGlobalVersion
|
||||
scriptsMu.RUnlock()
|
||||
instance.version = currentVersion
|
||||
p.mu.RUnlock()
|
||||
}
|
||||
|
||||
// 2. Set Context and default state
|
||||
@ -150,9 +156,80 @@ func Call(ctx context.Context, funcName string, args []any, opts ...CallOption)
|
||||
return result.Export(), nil
|
||||
}
|
||||
|
||||
// --- Global Proxy Functions ---
|
||||
|
||||
func Define(code string) {
|
||||
DefaultPool.Define(code)
|
||||
}
|
||||
|
||||
func Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||
return DefaultPool.Call(ctx, funcName, args, opts...)
|
||||
}
|
||||
|
||||
// --- Starter Interface Implementation ---
|
||||
|
||||
func (p *Pool) Start(ctx context.Context, logger *log.Logger) error {
|
||||
// For JS engine, start is mostly for pre-warming or registry checking.
|
||||
// We ensure the context is not canceled.
|
||||
if p.ctx.Err() != nil {
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
atomic.StoreInt32(&p.closed, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Pool) Stop(ctx context.Context) error {
|
||||
atomic.StoreInt32(&p.closed, 1)
|
||||
p.cancel() // Notify any long-running JS that are context-aware
|
||||
|
||||
// Wait for active Call() to finish or context timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
p.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("js.Pool: shutdown timeout, active tasks may still be running")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Status() (string, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return fmt.Sprintf("scripts: %d, version: %d, closed: %v", len(p.scripts), p.version, atomic.LoadInt32(&p.closed) == 1), nil
|
||||
}
|
||||
|
||||
// --- Helper types from original file ---
|
||||
|
||||
// CallOption allows configuring the JS execution environment.
|
||||
type CallOption func(vm *goja.Runtime)
|
||||
|
||||
// WithSafeMode enables or disables safe mode for the call.
|
||||
func WithSafeMode(enabled bool) CallOption {
|
||||
return func(vm *goja.Runtime) {
|
||||
_ = vm.Set("__safeMode__", enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger injects a custom logger for the call.
|
||||
func WithLogger(logger *log.Logger) CallOption {
|
||||
return func(vm *goja.Runtime) {
|
||||
_ = vm.Set("__logger__", vm.ToValue(logger))
|
||||
}
|
||||
}
|
||||
|
||||
// FuncList returns the list of all defined JS function names.
|
||||
func FuncList() []string {
|
||||
scriptsMu.RLock()
|
||||
defer scriptsMu.RUnlock()
|
||||
func (p *Pool) FuncList() []string {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
// Reflection to list functions in the latest script set could be added here
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func FuncList() []string {
|
||||
return DefaultPool.FuncList()
|
||||
}
|
||||
|
||||
51
shutdown_test.go
Normal file
51
shutdown_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPoolGracefulShutdown(t *testing.T) {
|
||||
p := NewPool()
|
||||
p.Define(`function sleep(ms) {
|
||||
var start = Date.now();
|
||||
while (Date.now() - start < ms);
|
||||
return "done";
|
||||
}`)
|
||||
|
||||
// Start a long running task
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := p.Call(context.Background(), "sleep", []any{500})
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Give it a moment to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Try to stop the pool
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
startStop := time.Now()
|
||||
if err := p.Stop(stopCtx); err != nil {
|
||||
t.Fatalf("Stop failed: %v", err)
|
||||
}
|
||||
stopDuration := time.Since(startStop)
|
||||
|
||||
if stopDuration < 300*time.Millisecond {
|
||||
t.Errorf("Stop returned too early, expected it to wait for task. Duration: %v", stopDuration)
|
||||
}
|
||||
|
||||
err := <-errChan
|
||||
if err != nil {
|
||||
t.Errorf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
// New calls should fail
|
||||
_, err = p.Call(context.Background(), "sleep", []any{10})
|
||||
if err == nil || err.Error() != "js.Pool: pool is closed" {
|
||||
t.Errorf("Expected 'pool is closed' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user