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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -9,23 +10,45 @@ import (
|
|||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Doc generates a TypeScript definition (.d.ts) for all registered Go modules.
|
// Doc generates a comprehensive TypeScript definition (.d.ts) for the Go/JS environment.
|
||||||
// This is designed to be fed into an AI (LLM) to provide context for low-code development.
|
|
||||||
func Doc() string {
|
func Doc() string {
|
||||||
var sb strings.Builder
|
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()
|
modules := jsmod.GetModules()
|
||||||
keys := make([]string, 0, len(modules))
|
modNames := make([]string, 0, len(modules))
|
||||||
for k := range 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")
|
ctx := &docCtx{
|
||||||
for _, modName := range keys {
|
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]
|
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))
|
expKeys := make([]string, 0, len(mod.Exports))
|
||||||
for k := range mod.Exports {
|
for k := range mod.Exports {
|
||||||
@ -34,57 +57,112 @@ func Doc() string {
|
|||||||
sort.Strings(expKeys)
|
sort.Strings(expKeys)
|
||||||
|
|
||||||
for _, name := range expKeys {
|
for _, name := range expKeys {
|
||||||
|
isHidden := strings.HasPrefix(name, "__export")
|
||||||
val := mod.Exports[name]
|
val := mod.Exports[name]
|
||||||
isUnsafe := mod.UnsafeList[name]
|
|
||||||
if isUnsafe {
|
memberDef := formatExport(name, val, ctx, false)
|
||||||
sb.WriteString(" /** @unsafe */\n")
|
|
||||||
|
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()
|
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)
|
t := reflect.TypeOf(val)
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return fmt.Sprintf("const %s: any;", name)
|
return fmt.Sprintf("%s: any;", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Kind() == reflect.Func {
|
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
|
var params []string
|
||||||
numIn := t.NumIn()
|
numIn := t.NumIn()
|
||||||
jsArgIdx := 0
|
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)
|
argType := t.In(i)
|
||||||
// Skip Context and Logger in TS doc
|
|
||||||
typeName := argType.String()
|
typeName := argType.String()
|
||||||
if typeName == "context.Context" || typeName == "*log.Logger" {
|
if typeName == "context.Context" || strings.Contains(typeName, "log.Logger") {
|
||||||
continue
|
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++
|
jsArgIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle return values
|
|
||||||
numOut := t.NumOut()
|
numOut := t.NumOut()
|
||||||
var retType string
|
var retType string
|
||||||
if numOut == 0 {
|
if numOut == 0 {
|
||||||
retType = "void"
|
retType = "void"
|
||||||
} else {
|
} else {
|
||||||
// If last return is error, we only care about the first part for TS doc
|
|
||||||
realOut := numOut
|
realOut := numOut
|
||||||
if numOut > 0 && t.Out(numOut-1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
if numOut > 0 && t.Out(numOut-1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||||
realOut--
|
realOut--
|
||||||
@ -93,7 +171,7 @@ func formatFunc(t reflect.Type) string {
|
|||||||
if realOut <= 0 {
|
if realOut <= 0 {
|
||||||
retType = "void"
|
retType = "void"
|
||||||
} else if realOut == 1 {
|
} else if realOut == 1 {
|
||||||
retType = goTypeToTS(t.Out(0))
|
retType = goTypeToTS(t.Out(0), ctx)
|
||||||
} else {
|
} else {
|
||||||
retType = "any[]"
|
retType = "any[]"
|
||||||
}
|
}
|
||||||
@ -102,43 +180,169 @@ func formatFunc(t reflect.Type) string {
|
|||||||
return fmt.Sprintf("(%s): %s", strings.Join(params, ", "), retType)
|
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 {
|
if t == nil {
|
||||||
return "any"
|
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 {
|
for t.Kind() == reflect.Ptr {
|
||||||
t = t.Elem()
|
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() {
|
switch t.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return "string"
|
return "string"
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||||
reflect.Float32, reflect.Float64:
|
reflect.Float32, reflect.Float64:
|
||||||
|
// time.Duration is an Int64, but we treat it as number (ms)
|
||||||
return "number"
|
return "number"
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return "boolean"
|
return "boolean"
|
||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
return goTypeToTS(t.Elem()) + "[]"
|
return goTypeToTS(t.Elem(), ctx) + "[]"
|
||||||
case reflect.Map:
|
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:
|
case reflect.Struct:
|
||||||
return "{ [key: string]: any }"
|
if isAnonymous {
|
||||||
|
return "{ [key: string]: any }"
|
||||||
|
}
|
||||||
|
return registerInterface(t, ctx)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
return "any"
|
return "any"
|
||||||
default:
|
default:
|
||||||
return "any"
|
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
|
return nil, nil
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"__exportInternal": func() *struct{ Name string } { return nil },
|
||||||
})
|
})
|
||||||
|
|
||||||
doc := Doc()
|
doc := Doc()
|
||||||
fmt.Println(doc)
|
fmt.Println(doc)
|
||||||
|
|
||||||
if !strings.Contains(doc, "namespace db") {
|
if !strings.Contains(doc, "interface GoTime") {
|
||||||
t.Error("doc should contain namespace db")
|
t.Error("doc should contain GoTime interface")
|
||||||
}
|
}
|
||||||
if !strings.Contains(doc, "function query(arg0: string, arg1: any[]): Record<string, any>[];") {
|
if !strings.Contains(doc, "interface Db_Module") {
|
||||||
t.Error("doc should contain query function with correct signature")
|
t.Error("doc should contain Db_Module interface")
|
||||||
}
|
}
|
||||||
if !strings.Contains(doc, "const version: string;") {
|
if !strings.Contains(doc, "declare const go:") {
|
||||||
t.Error("doc should contain version constant")
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
|
"apigo.cc/go/log"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,12 +16,25 @@ type vmInstance struct {
|
|||||||
version int32
|
version int32
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// Pool represents an isolated JS execution environment with its own script registry and VM pool.
|
||||||
globalVersion int32
|
type Pool struct {
|
||||||
scripts []string
|
version int32
|
||||||
scriptsMu sync.RWMutex
|
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 {
|
New: func() any {
|
||||||
return &vmInstance{
|
return &vmInstance{
|
||||||
runtime: createNewRuntime(),
|
runtime: createNewRuntime(),
|
||||||
@ -29,7 +42,11 @@ var (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultPool is the global singleton pool.
|
||||||
|
var DefaultPool = NewPool()
|
||||||
|
|
||||||
func createNewRuntime() *goja.Runtime {
|
func createNewRuntime() *goja.Runtime {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
@ -55,54 +72,43 @@ func createNewRuntime() *goja.Runtime {
|
|||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define adds JS code to the global registry and increments the version.
|
// Define adds JS code to the pool's registry and increments the version.
|
||||||
// All VMs in the pool will eventually synchronize to this version.
|
func (p *Pool) Define(code string) {
|
||||||
func Define(code string) {
|
p.mu.Lock()
|
||||||
scriptsMu.Lock()
|
defer p.mu.Unlock()
|
||||||
defer scriptsMu.Unlock()
|
|
||||||
|
|
||||||
scripts = append(scripts, code)
|
p.scripts = append(p.scripts, code)
|
||||||
atomic.AddInt32(&globalVersion, 1)
|
atomic.AddInt32(&p.version, 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call executes a JS function from the pool.
|
// Call executes a JS function from the pool.
|
||||||
// It automatically synchronizes the VM to the latest version.
|
func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||||
func Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
if atomic.LoadInt32(&p.closed) == 1 {
|
||||||
instance := pool.Get().(*vmInstance)
|
return nil, fmt.Errorf("js.Pool: pool is closed")
|
||||||
defer pool.Put(instance)
|
}
|
||||||
|
|
||||||
|
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
|
vm := instance.runtime
|
||||||
|
|
||||||
// 1. Synchronize scripts if version is behind
|
// 1. Synchronize scripts if version is behind
|
||||||
currentGlobalVersion := atomic.LoadInt32(&globalVersion)
|
currentVersion := atomic.LoadInt32(&p.version)
|
||||||
if instance.version < currentGlobalVersion {
|
if instance.version < currentVersion {
|
||||||
scriptsMu.RLock()
|
p.mu.RLock()
|
||||||
for i := int(instance.version); i < len(scripts); i++ {
|
for i := int(instance.version); i < len(p.scripts); i++ {
|
||||||
_, err := vm.RunString(scripts[i])
|
_, err := vm.RunString(p.scripts[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scriptsMu.RUnlock()
|
p.mu.RUnlock()
|
||||||
return nil, fmt.Errorf("js.sync error at script %d: %w", i, err)
|
return nil, fmt.Errorf("js.sync error at script %d: %w", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instance.version = currentGlobalVersion
|
instance.version = currentVersion
|
||||||
scriptsMu.RUnlock()
|
p.mu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Set Context and default state
|
// 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
|
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.
|
// FuncList returns the list of all defined JS function names.
|
||||||
func FuncList() []string {
|
func (p *Pool) FuncList() []string {
|
||||||
scriptsMu.RLock()
|
p.mu.RLock()
|
||||||
defer scriptsMu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
|
// Reflection to list functions in the latest script set could be added here
|
||||||
return []string{}
|
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