Compare commits
No commits in common. "main" and "v1.0.2" have entirely different histories.
27
CHANGELOG.md
27
CHANGELOG.md
@ -1,27 +0,0 @@
|
|||||||
# CHANGELOG - go/js
|
|
||||||
|
|
||||||
## v1.5.2 (2026-06-08)
|
|
||||||
- **API 增强: 脚本版本管理与发现**:
|
|
||||||
- `Define` 方法现在支持可选的 `name` 和 `version` (int64) 参数。
|
|
||||||
- 增加 `CheckVersion(name string, version int64) bool` 用于检查脚本是否已加载且版本匹配,减少重复加载。
|
|
||||||
- 实现 `FuncList() []string`,支持动态发现脚本中定义的函数名(基于正则提取)。
|
|
||||||
- **执行安全: Context 中断支持**:
|
|
||||||
- `Call` 方法现在支持 `context.Context` 中断。如果 context 被取消或超时,JS 执行将被立即中断。
|
|
||||||
- **性能优化**:
|
|
||||||
- 优化 `createNewRuntime` 中的反射检查逻辑。
|
|
||||||
- 优化 `Call` 方法,在 Context 不可取消时避免创建额外的 goroutine。
|
|
||||||
- **稳定性**:
|
|
||||||
- 完善 `Pool` 状态上报,包含已加载脚本和函数数量。
|
|
||||||
|
|
||||||
## 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`)导致反射崩溃的问题。
|
|
||||||
21
README.md
21
README.md
@ -8,9 +8,7 @@ A lightweight, frictionless, and AI-friendly JavaScript engine for Go applicatio
|
|||||||
- **Frictionless Bridging**: Automatic type conversion using `go/cast`.
|
- **Frictionless Bridging**: Automatic type conversion using `go/cast`.
|
||||||
- **Host Object Fidelity**: Go pointers and structs are preserved when passed back and forth between Go and JS.
|
- **Host Object Fidelity**: Go pointers and structs are preserved when passed back and forth between Go and JS.
|
||||||
- **Context Injection**: Automatic `context.Context` propagation from `js.Call`.
|
- **Context Injection**: Automatic `context.Context` propagation from `js.Call`.
|
||||||
- **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`).
|
- **Versioned Pool**: Thread-safe VM pool with incremental code synchronization.
|
||||||
- **Function Discovery**: List all defined functions via `FuncList()`.
|
|
||||||
- **Context Interruption**: Safe execution with `context.Context` cancellation support.
|
|
||||||
- **AI-Ready**: Generates TypeScript definitions (`.d.ts`) for AI to understand available capabilities.
|
- **AI-Ready**: Generates TypeScript definitions (`.d.ts`) for AI to understand available capabilities.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -29,28 +27,23 @@ func init() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Execute JS with Version Checking
|
### 2. Execute JS
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "apigo.cc/go/js"
|
import "apigo.cc/go/js"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Check if script needs update (e.g., from file mtime)
|
js.Define(`
|
||||||
if !js.CheckVersion("myTask.js", mtime) {
|
function myTask(name) {
|
||||||
js.Define(code, "myTask.js", mtime)
|
let data = go.db.query("SELECT * FROM users WHERE name = ?", [name]);
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
res, err := js.Call(ctx, "myTask", "star")
|
res, err := js.Call(ctx, "myTask", "star")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Discover Functions
|
|
||||||
|
|
||||||
```go
|
|
||||||
funcs := js.FuncList()
|
|
||||||
// ["myTask", ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Generate AI Context
|
### 3. Generate AI Context
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
45
TEST.md
45
TEST.md
@ -1,45 +0,0 @@
|
|||||||
# Test Report - go/js
|
|
||||||
|
|
||||||
## Performance (Benchmark)
|
|
||||||
Date: 2026-06-08
|
|
||||||
OS: darwin
|
|
||||||
Arch: amd64
|
|
||||||
CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
|
||||||
|
|
||||||
| Benchmark | Iterations | Time/op |
|
|
||||||
|-----------|------------|---------|
|
|
||||||
| BenchmarkCall | 990318 | 1109 ns/op |
|
|
||||||
| BenchmarkSync | 57362 | 78846 ns/op |
|
|
||||||
|
|
||||||
*Note: BenchmarkCall covers the hot path of executing a JS function from the pool. BenchmarkSync covers the cost of defining new code (including regex parsing) and syncing a VM.*
|
|
||||||
|
|
||||||
## Coverage
|
|
||||||
```
|
|
||||||
=== RUN TestBridgeSafeMode
|
|
||||||
--- PASS: TestBridgeSafeMode (0.00s)
|
|
||||||
=== RUN TestBridgeLoggerInjection
|
|
||||||
--- PASS: TestBridgeLoggerInjection (0.00s)
|
|
||||||
=== RUN TestBridgeMixedInjection
|
|
||||||
--- PASS: TestBridgeMixedInjection (0.00s)
|
|
||||||
=== RUN TestDocGeneration
|
|
||||||
--- PASS: TestDocGeneration (0.00s)
|
|
||||||
=== RUN TestPoolVersioning
|
|
||||||
--- PASS: TestPoolVersioning (0.00s)
|
|
||||||
=== RUN TestPoolConcurrent
|
|
||||||
--- PASS: TestPoolConcurrent (0.00s)
|
|
||||||
=== RUN TestPoolGracefulShutdown
|
|
||||||
--- PASS: TestPoolGracefulShutdown (0.50s)
|
|
||||||
PASS
|
|
||||||
ok apigo.cc/go/js 0.932s
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features Verified
|
|
||||||
- [x] JS calling Go with automatic type conversion.
|
|
||||||
- [x] Go pointers preservation in JS.
|
|
||||||
- [x] Context and Logger injection.
|
|
||||||
- [x] Concurrent execution and script versioning.
|
|
||||||
- [x] Script version checking (`CheckVersion`).
|
|
||||||
- [x] Function discovery (`FuncList`).
|
|
||||||
- [x] Context cancellation interruption.
|
|
||||||
- [x] Graceful shutdown.
|
|
||||||
- [x] TypeScript definition generation.
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package js
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkCall(b *testing.B) {
|
|
||||||
p := NewPool()
|
|
||||||
p.Define(`function add(a, b) { return a + b; }`)
|
|
||||||
args := []any{1, 2}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := p.Call("add", 0, nil, args...)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkSync(b *testing.B) {
|
|
||||||
p := NewPool()
|
|
||||||
code := `function f() { return 1; }`
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
p.Define(code)
|
|
||||||
_, err := p.Call("f", 0, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
bridge.go
32
bridge.go
@ -3,11 +3,12 @@ package js
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
"apigo.cc/go/log"
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,12 +25,10 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
|||||||
return vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
return vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||||
// 1. Safety Check
|
// 1. Safety Check
|
||||||
safeMode := true // Default to safe mode
|
safeMode := true // Default to safe mode
|
||||||
ctxVal := vm.Get("__ctx__")
|
smVal := vm.Get("__safeMode__")
|
||||||
var currentCtx context.Context
|
if smVal != nil && !goja.IsUndefined(smVal) {
|
||||||
if ctxVal != nil && !goja.IsUndefined(ctxVal) {
|
if sm, ok := smVal.Export().(bool); ok {
|
||||||
if c, ok := ctxVal.Export().(context.Context); ok {
|
safeMode = sm
|
||||||
currentCtx = c
|
|
||||||
safeMode = jsmod.IsSafeMode(c)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,10 +47,15 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
|||||||
|
|
||||||
// Magic Injection: context.Context
|
// Magic Injection: context.Context
|
||||||
if argType.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
|
if argType.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
|
||||||
ctx := currentCtx
|
ctx := context.Background()
|
||||||
if ctx == nil {
|
ctxVal := vm.Get("__ctx__")
|
||||||
ctx = context.Background()
|
if ctxVal != nil && !goja.IsUndefined(ctxVal) {
|
||||||
|
if c, ok := ctxVal.Export().(context.Context); ok {
|
||||||
|
ctx = c
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Inject SafeMode status into context
|
||||||
|
ctx = context.WithValue(ctx, jsmod.SafeModeKey, safeMode)
|
||||||
goArgs[i] = reflect.ValueOf(ctx)
|
goArgs[i] = reflect.ValueOf(ctx)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -59,13 +63,15 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
|||||||
// Magic Injection: *log.Logger
|
// Magic Injection: *log.Logger
|
||||||
if argType == reflect.TypeOf((*log.Logger)(nil)) {
|
if argType == reflect.TypeOf((*log.Logger)(nil)) {
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
if currentCtx != nil {
|
logVal := vm.Get("__logger__")
|
||||||
if l, ok := jsmod.Get(currentCtx, "Logger").(*log.Logger); ok {
|
if logVal != nil && !goja.IsUndefined(logVal) {
|
||||||
|
if l, ok := logVal.Export().(*log.Logger); ok {
|
||||||
logger = l
|
logger = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.DefaultLogger
|
// Fallback to a discard logger if none provided to avoid nil panic in Go side
|
||||||
|
logger = log.New(io.Discard, "", 0)
|
||||||
}
|
}
|
||||||
goArgs[i] = reflect.ValueOf(logger)
|
goArgs[i] = reflect.ValueOf(logger)
|
||||||
continue
|
continue
|
||||||
|
|||||||
107
bridge_test.go
107
bridge_test.go
@ -3,106 +3,87 @@ package js
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
"apigo.cc/go/jsmod"
|
|
||||||
"apigo.cc/go/log"
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridgeSafeMode(t *testing.T) {
|
func TestBridgeSafeMode(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
|
|
||||||
// Set up safe context
|
unsafeFn := func() string { return "danger" }
|
||||||
injects := map[string]any{"SafeMode": true}
|
|
||||||
ctx := jsmod.NewContext(context.Background(), injects)
|
|
||||||
vm.Set("__ctx__", vm.ToValue(ctx))
|
|
||||||
|
|
||||||
unsafeFn := func() error { return nil }
|
// Register with isUnsafe = true
|
||||||
vm.Set("unsafe", wrapGoFunc(vm, unsafeFn, true))
|
vm.Set("danger", wrapGoFunc(vm, unsafeFn, true))
|
||||||
|
|
||||||
_, err := vm.RunString(`unsafe()`)
|
// 1. Default (SafeMode = true)
|
||||||
if err == nil {
|
_, err := vm.RunString(`danger()`)
|
||||||
t.Error("SafeMode failed to block unsafe function")
|
if err == nil || !strings.Contains(err.Error(), "blocked by safeMode") {
|
||||||
} else if !strings.Contains(err.Error(), "unauthorized") {
|
t.Fatalf("Expected safeMode block, got: %v", err)
|
||||||
t.Errorf("Expected unauthorized error, got %v", err)
|
}
|
||||||
|
|
||||||
|
// 2. Disable SafeMode
|
||||||
|
vm.Set("__safeMode__", false)
|
||||||
|
val, err := vm.RunString(`danger()`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if val.Export() != "danger" {
|
||||||
|
t.Errorf("Expected 'danger', got %v", val.Export())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridgeLoggerInjection(t *testing.T) {
|
func TestBridgeLoggerInjection(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
logger := log.New("test")
|
logger := log.New(&buf, "", 0)
|
||||||
log.SetStdLogOutput(&buf) // Capture through std log for simplicity in test
|
|
||||||
|
|
||||||
// Inject logger via context
|
vm.Set("__logger__", vm.ToValue(logger))
|
||||||
injects := map[string]any{"Logger": logger}
|
|
||||||
ctx := jsmod.NewContext(context.Background(), injects)
|
|
||||||
vm.Set("__ctx__", vm.ToValue(ctx))
|
|
||||||
|
|
||||||
logFn := func(l *log.Logger) {
|
logFn := func(l *log.Logger, msg string) {
|
||||||
l.Info("hello from js")
|
l.Print(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.Set("log", wrapGoFunc(vm, logFn, false))
|
vm.Set("logMsg", wrapGoFunc(vm, logFn, false))
|
||||||
_, err := vm.RunString(`log()`)
|
|
||||||
|
// JS only passes the 'msg' argument, logger is injected
|
||||||
|
_, err := vm.RunString(`logMsg("hello from js")`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JS execution failed: %v", err)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(buf.String(), "hello from js") {
|
||||||
|
t.Errorf("Logger injection failed, buffer: %s", buf.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridgeMixedInjection(t *testing.T) {
|
func TestBridgeMixedInjection(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
|
ctx := context.WithValue(context.Background(), "k", "v")
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := log.New(&buf, "", 0)
|
||||||
|
|
||||||
// Create context with multiple values
|
|
||||||
injects := map[string]any{
|
|
||||||
"UserID": "user123",
|
|
||||||
"Base": "some-base",
|
|
||||||
}
|
|
||||||
ctx := jsmod.NewContext(context.Background(), injects)
|
|
||||||
vm.Set("__ctx__", vm.ToValue(ctx))
|
vm.Set("__ctx__", vm.ToValue(ctx))
|
||||||
|
vm.Set("__logger__", vm.ToValue(logger))
|
||||||
|
|
||||||
mixedFn := func(c context.Context, a int) string {
|
mixedFn := func(c context.Context, l *log.Logger, a int) string {
|
||||||
uid := cast.String(jsmod.Get(c, "UserID"))
|
l.Printf("val: %d", a)
|
||||||
return fmt.Sprintf("%s:%d", uid, a)
|
return c.Value("k").(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.Set("mixed", wrapGoFunc(vm, mixedFn, false))
|
vm.Set("mixed", wrapGoFunc(vm, mixedFn, false))
|
||||||
|
|
||||||
val, err := vm.RunString(`mixed(42)`)
|
val, err := vm.RunString(`mixed(42)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JS execution failed: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if val.Export() != "user123:42" {
|
if val.Export() != "v" {
|
||||||
t.Errorf("Mixed injection failed, got %v", val.Export())
|
t.Errorf("Context injection failed")
|
||||||
}
|
}
|
||||||
}
|
if !strings.Contains(buf.String(), "val: 42") {
|
||||||
|
t.Errorf("Logger injection failed")
|
||||||
func TestBridgeOptionalParams(t *testing.T) {
|
|
||||||
vm := goja.New()
|
|
||||||
|
|
||||||
optionalFn := func(a int, b *string) string {
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Sprintf("%d:nil", a)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d:%s", a, *b)
|
|
||||||
}
|
|
||||||
|
|
||||||
vm.Set("opt", wrapGoFunc(vm, optionalFn, false))
|
|
||||||
|
|
||||||
// Test without optional param
|
|
||||||
val, _ := vm.RunString(`opt(1)`)
|
|
||||||
if val.Export() != "1:nil" {
|
|
||||||
t.Errorf("Optional param failed (nil), got %v", val.Export())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with optional param
|
|
||||||
val, _ = vm.RunString(`opt(2, "hello")`)
|
|
||||||
if val.Export() != "2:hello" {
|
|
||||||
t.Errorf("Optional param failed (val), got %v", val.Export())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
293
doc.go
293
doc.go
@ -2,7 +2,6 @@ package js
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,45 +9,23 @@ import (
|
|||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Doc generates a comprehensive TypeScript definition (.d.ts) for the Go/JS environment.
|
// 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.
|
||||||
func Doc() string {
|
func Doc() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("/**\n * Go/JS Low-Code Environment Type Definitions\n")
|
sb.WriteString("// TypeScript Definitions for Go/JS Low-Code Environment\n\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()
|
||||||
modNames := make([]string, 0, len(modules))
|
keys := make([]string, 0, len(modules))
|
||||||
for k := range modules {
|
for k := range modules {
|
||||||
modNames = append(modNames, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
sort.Strings(modNames)
|
sort.Strings(keys)
|
||||||
|
|
||||||
ctx := &docCtx{
|
sb.WriteString("declare namespace go {\n")
|
||||||
seenTypes: make(map[reflect.Type]string),
|
for _, modName := range keys {
|
||||||
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]
|
||||||
ctx.currentMod = modName
|
sb.WriteString(fmt.Sprintf(" namespace %s {\n", 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 {
|
||||||
@ -57,125 +34,57 @@ 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]
|
||||||
|
|
||||||
memberDef := formatExport(name, val, ctx, false)
|
|
||||||
|
|
||||||
if !isHidden {
|
|
||||||
isUnsafe := mod.UnsafeList[name]
|
isUnsafe := mod.UnsafeList[name]
|
||||||
if isUnsafe {
|
if isUnsafe {
|
||||||
msb.WriteString(" /** @unsafe */\n")
|
sb.WriteString(" /** @unsafe */\n")
|
||||||
}
|
}
|
||||||
msb.WriteString(fmt.Sprintf(" %s\n", memberDef))
|
sb.WriteString(fmt.Sprintf(" %s\n", formatExport(name, val)))
|
||||||
}
|
}
|
||||||
|
sb.WriteString(" }\n")
|
||||||
}
|
}
|
||||||
msb.WriteString("}")
|
sb.WriteString("}\n")
|
||||||
moduleDefs[modName] = msb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
type docCtx struct {
|
func formatExport(name string, val any) string {
|
||||||
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("%s: any;", name)
|
return fmt.Sprintf("const %s: any;", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Kind() == reflect.Func {
|
if t.Kind() == reflect.Func {
|
||||||
return fmt.Sprintf("%s%s;", name, formatFunc(t, ctx, isMethod))
|
return fmt.Sprintf("function %s%s;", name, formatFunc(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s: %s;", name, goTypeToTS(t, ctx))
|
return fmt.Sprintf("const %s: %s;", name, goTypeToTS(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatFunc(t reflect.Type, ctx *docCtx, isMethod bool) string {
|
func formatFunc(t reflect.Type) string {
|
||||||
var params []string
|
var params []string
|
||||||
numIn := t.NumIn()
|
numIn := t.NumIn()
|
||||||
jsArgIdx := 0
|
jsArgIdx := 0
|
||||||
|
|
||||||
startIdx := 0
|
for i := 0; i < numIn; i++ {
|
||||||
if isMethod {
|
|
||||||
startIdx = 1 // Skip receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
isVariadic := t.IsVariadic()
|
|
||||||
|
|
||||||
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" || strings.Contains(typeName, "log.Logger") {
|
if typeName == "context.Context" || typeName == "*log.Logger" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
isLast := i == numIn-1
|
params = append(params, fmt.Sprintf("arg%d: %s", jsArgIdx, goTypeToTS(argType)))
|
||||||
paramName := fmt.Sprintf("arg%d", jsArgIdx)
|
|
||||||
|
|
||||||
if isVariadic && isLast {
|
|
||||||
// Variadic parameters are optional in TS
|
|
||||||
params = append(params, fmt.Sprintf("...%s: %s", paramName, goTypeToTS(argType.Elem(), ctx)))
|
|
||||||
} else if argType.Kind() == reflect.Ptr {
|
|
||||||
// Pointer parameters at the end are optional
|
|
||||||
params = append(params, fmt.Sprintf("%s?: %s", paramName, goTypeToTS(argType, ctx)))
|
|
||||||
} else {
|
|
||||||
params = append(params, fmt.Sprintf("%s: %s", paramName, 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--
|
||||||
@ -184,7 +93,7 @@ func formatFunc(t reflect.Type, ctx *docCtx, isMethod bool) string {
|
|||||||
if realOut <= 0 {
|
if realOut <= 0 {
|
||||||
retType = "void"
|
retType = "void"
|
||||||
} else if realOut == 1 {
|
} else if realOut == 1 {
|
||||||
retType = goTypeToTS(t.Out(0), ctx)
|
retType = goTypeToTS(t.Out(0))
|
||||||
} else {
|
} else {
|
||||||
retType = "any[]"
|
retType = "any[]"
|
||||||
}
|
}
|
||||||
@ -193,169 +102,43 @@ func formatFunc(t reflect.Type, ctx *docCtx, isMethod bool) string {
|
|||||||
return fmt.Sprintf("(%s): %s", strings.Join(params, ", "), retType)
|
return fmt.Sprintf("(%s): %s", strings.Join(params, ", "), retType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func goTypeToTS(t reflect.Type, ctx *docCtx) string {
|
func goTypeToTS(t reflect.Type) 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(), ctx) + "[]"
|
return goTypeToTS(t.Elem()) + "[]"
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
// http.Header as a special Record mapping
|
return "Record<string, any>"
|
||||||
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:
|
||||||
if isAnonymous {
|
|
||||||
return "{ [key: string]: any }"
|
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,25 +15,18 @@ 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, "interface GoTime") {
|
if !strings.Contains(doc, "namespace db") {
|
||||||
t.Error("doc should contain GoTime interface")
|
t.Error("doc should contain namespace db")
|
||||||
}
|
}
|
||||||
if !strings.Contains(doc, "interface Db_Module") {
|
if !strings.Contains(doc, "function query(arg0: string, arg1: any[]): Record<string, any>[];") {
|
||||||
t.Error("doc should contain Db_Module interface")
|
t.Error("doc should contain query function with correct signature")
|
||||||
}
|
}
|
||||||
if !strings.Contains(doc, "declare const go:") {
|
if !strings.Contains(doc, "const version: string;") {
|
||||||
t.Error("doc should contain global go declaration")
|
t.Error("doc should contain version constant")
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
go.mod
15
go.mod
@ -3,25 +3,14 @@ module apigo.cc/go/js
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.5.0
|
apigo.cc/go/cast v1.3.3
|
||||||
apigo.cc/go/jsmod v1.5.0
|
apigo.cc/go/jsmod v1.0.0
|
||||||
apigo.cc/go/log v1.5.5
|
|
||||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/config v1.5.0 // indirect
|
|
||||||
apigo.cc/go/encoding v1.5.0 // indirect
|
|
||||||
apigo.cc/go/file v1.5.0 // indirect
|
|
||||||
apigo.cc/go/id v1.5.0 // indirect
|
|
||||||
apigo.cc/go/rand v1.5.0 // indirect
|
|
||||||
apigo.cc/go/safe v1.5.0 // indirect
|
|
||||||
apigo.cc/go/shell v1.5.0 // indirect
|
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
44
go.sum
44
go.sum
@ -1,49 +1,9 @@
|
|||||||
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
|
||||||
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
apigo.cc/go/jsmod v1.0.0 h1:lVQMq0tCno4kbHlQ3j5wzsm+v24J+bznIoHxpton0pE=
|
||||||
apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
|
|
||||||
apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
|
|
||||||
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
|
||||||
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
|
|
||||||
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
|
|
||||||
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
|
|
||||||
apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
|
|
||||||
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
|
|
||||||
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
|
||||||
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
|
||||||
apigo.cc/go/log v1.5.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI=
|
|
||||||
apigo.cc/go/log v1.5.5/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
|
|
||||||
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
|
||||||
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
|
||||||
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
|
|
||||||
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
|
|
||||||
apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q=
|
|
||||||
apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
||||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
250
pool.go
250
pool.go
@ -3,53 +3,25 @@ package js
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"log"
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
"apigo.cc/go/log"
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
type scriptEntry struct {
|
|
||||||
name string
|
|
||||||
code string
|
|
||||||
version int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type vmInstance struct {
|
type vmInstance struct {
|
||||||
runtime *goja.Runtime
|
runtime *goja.Runtime
|
||||||
version int32
|
version int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool represents an isolated JS execution environment with its own script registry and VM pool.
|
var (
|
||||||
type Pool struct {
|
globalVersion int32
|
||||||
version int32
|
scripts []string
|
||||||
scripts []*scriptEntry
|
scriptsMu sync.RWMutex
|
||||||
scriptMap map[string]*scriptEntry
|
|
||||||
functions map[string]struct{}
|
|
||||||
mu sync.RWMutex
|
|
||||||
pool sync.Pool
|
|
||||||
|
|
||||||
// Lifecycle management
|
pool = sync.Pool{
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
closed int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPool creates a new isolated JS execution environment.
|
|
||||||
func NewPool() *Pool {
|
|
||||||
p := &Pool{
|
|
||||||
scriptMap: make(map[string]*scriptEntry),
|
|
||||||
functions: make(map[string]struct{}),
|
|
||||||
}
|
|
||||||
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(),
|
||||||
@ -57,11 +29,7 @@ func NewPool() *Pool {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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()
|
||||||
@ -75,138 +43,79 @@ func createNewRuntime() *goja.Runtime {
|
|||||||
modObj := vm.NewObject()
|
modObj := vm.NewObject()
|
||||||
for name, val := range mod.Exports {
|
for name, val := range mod.Exports {
|
||||||
isUnsafe := mod.UnsafeList[name]
|
isUnsafe := mod.UnsafeList[name]
|
||||||
if val != nil && reflect.TypeOf(val).Kind() == reflect.Func {
|
if reflectType := fmt.Sprintf("%T", val); reflectType == "func" || (len(reflectType) > 4 && reflectType[:4] == "func") {
|
||||||
_ = modObj.Set(name, wrapGoFunc(vm, val, isUnsafe))
|
_ = modObj.Set(name, wrapGoFunc(vm, val, isUnsafe))
|
||||||
} else {
|
} else {
|
||||||
_ = modObj.Set(name, vm.ToValue(val))
|
_ = modObj.Set(name, vm.ToValue(val))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = goObj.Set(modName, modObj)
|
_ = goObj.Set(modName, modObj)
|
||||||
_ = vm.Set(modName, modObj) // Also inject into global
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
var funcRegex = regexp.MustCompile(`function\s+([a-zA-Z0-9_]+)\s*\(`)
|
// Define adds JS code to the global registry and increments the version.
|
||||||
var constFuncRegex = regexp.MustCompile(`(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*(?:function|\([^)]*\)\s*=>)`)
|
// 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.
|
scripts = append(scripts, code)
|
||||||
// name and version are optional and used for CheckVersion.
|
atomic.AddInt32(&globalVersion, 1)
|
||||||
func (p *Pool) Define(code string, args ...any) {
|
}
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
name := ""
|
// CallOption allows configuring the JS execution environment.
|
||||||
version := int64(0)
|
type CallOption func(vm *goja.Runtime)
|
||||||
if len(args) > 0 {
|
|
||||||
if s, ok := args[0].(string); ok {
|
// WithSafeMode enables or disables safe mode for the call.
|
||||||
name = s
|
func WithSafeMode(enabled bool) CallOption {
|
||||||
}
|
return func(vm *goja.Runtime) {
|
||||||
}
|
_ = vm.Set("__safeMode__", enabled)
|
||||||
if len(args) > 1 {
|
|
||||||
if v, ok := args[1].(int64); ok {
|
|
||||||
version = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := &scriptEntry{name: name, code: code, version: version}
|
// WithLogger injects a custom logger for the call.
|
||||||
p.scripts = append(p.scripts, entry)
|
func WithLogger(logger *log.Logger) CallOption {
|
||||||
if name != "" {
|
return func(vm *goja.Runtime) {
|
||||||
p.scriptMap[name] = entry
|
_ = vm.Set("__logger__", vm.ToValue(logger))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract functions for FuncList
|
|
||||||
matches := funcRegex.FindAllStringSubmatch(code, -1)
|
|
||||||
for _, m := range matches {
|
|
||||||
if len(m) > 1 {
|
|
||||||
p.functions[m[1]] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matches = constFuncRegex.FindAllStringSubmatch(code, -1)
|
|
||||||
for _, m := range matches {
|
|
||||||
if len(m) > 1 {
|
|
||||||
p.functions[m[1]] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt32(&p.version, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckVersion returns true if a script with the given name exists and its version is >= the provided version.
|
|
||||||
func (p *Pool) CheckVersion(name string, version int64) bool {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
if entry, ok := p.scriptMap[name]; ok {
|
|
||||||
return entry.version >= version
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckVersion(name string, version int64) bool {
|
|
||||||
return DefaultPool.CheckVersion(name, version)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call executes a JS function from the pool.
|
// Call executes a JS function from the pool.
|
||||||
// It combines the pool's lifecycle context with the provided timeout.
|
// It automatically synchronizes the VM to the latest version.
|
||||||
// injects are added to the context passed to Go functions.
|
func Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||||
func (p *Pool) Call(funcName string, timeout time.Duration, injects map[string]any, args ...any) (any, error) {
|
instance := pool.Get().(*vmInstance)
|
||||||
if atomic.LoadInt32(&p.closed) == 1 {
|
defer pool.Put(instance)
|
||||||
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
|
vm := instance.runtime
|
||||||
vm.ClearInterrupt()
|
|
||||||
|
|
||||||
// 1. Synchronize scripts if version is behind
|
// 1. Synchronize scripts if version is behind
|
||||||
currentVersion := atomic.LoadInt32(&p.version)
|
currentGlobalVersion := atomic.LoadInt32(&globalVersion)
|
||||||
if instance.version < currentVersion {
|
if instance.version < currentGlobalVersion {
|
||||||
p.mu.RLock()
|
scriptsMu.RLock()
|
||||||
for i := int(instance.version); i < len(p.scripts); i++ {
|
for i := int(instance.version); i < len(scripts); i++ {
|
||||||
_, err := vm.RunString(p.scripts[i].code)
|
_, err := vm.RunString(scripts[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.mu.RUnlock()
|
scriptsMu.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 = currentVersion
|
instance.version = currentGlobalVersion
|
||||||
p.mu.RUnlock()
|
scriptsMu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Prepare Context
|
// 2. Set Context and default state
|
||||||
execCtx := jsmod.NewContext(p.ctx, injects)
|
_ = vm.Set("__ctx__", vm.ToValue(ctx))
|
||||||
var cancel context.CancelFunc
|
_ = vm.Set("__safeMode__", true) // Default is safe
|
||||||
if timeout > 0 {
|
_ = vm.Set("__logger__", goja.Undefined())
|
||||||
execCtx, cancel = context.WithTimeout(execCtx, timeout)
|
|
||||||
defer cancel()
|
// Apply Options
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Set VM environment
|
// 3. Get and Call JS Function
|
||||||
_ = vm.Set("__ctx__", vm.ToValue(execCtx))
|
|
||||||
|
|
||||||
// 4. Set up interruption
|
|
||||||
stopInterrupter := make(chan struct{})
|
|
||||||
defer close(stopInterrupter)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-execCtx.Done():
|
|
||||||
reason := "execution timeout/canceled"
|
|
||||||
if p.ctx.Err() != nil {
|
|
||||||
reason = "application stopping"
|
|
||||||
}
|
|
||||||
vm.Interrupt(reason)
|
|
||||||
case <-stopInterrupter:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 5. Get and Call JS Function
|
|
||||||
fnVal := vm.Get(funcName)
|
fnVal := vm.Get(funcName)
|
||||||
if fnVal == nil || goja.IsUndefined(fnVal) {
|
if fnVal == nil || goja.IsUndefined(fnVal) {
|
||||||
return nil, fmt.Errorf("js.Call: function '%s' not found", funcName)
|
return nil, fmt.Errorf("js.Call: function '%s' not found", funcName)
|
||||||
@ -222,7 +131,7 @@ func (p *Pool) Call(funcName string, timeout time.Duration, injects map[string]a
|
|||||||
jsArgs[i] = vm.ToValue(arg)
|
jsArgs[i] = vm.ToValue(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Execution with error capture
|
// 4. Execution with error capture
|
||||||
var result goja.Value
|
var result goja.Value
|
||||||
var err error
|
var err error
|
||||||
func() {
|
func() {
|
||||||
@ -241,64 +150,9 @@ func (p *Pool) Call(funcName string, timeout time.Duration, injects map[string]a
|
|||||||
return result.Export(), nil
|
return result.Export(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Global Proxy Functions ---
|
|
||||||
|
|
||||||
func Define(code string, args ...any) {
|
|
||||||
DefaultPool.Define(code, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Call(funcName string, timeout time.Duration, injects map[string]any, args ...any) (any, error) {
|
|
||||||
return DefaultPool.Call(funcName, timeout, injects, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Starter Interface Implementation ---
|
|
||||||
|
|
||||||
func (p *Pool) Start(ctx context.Context, logger *log.Logger) error {
|
|
||||||
// Ensure pool context is fresh
|
|
||||||
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() // Stop all active and future calls
|
|
||||||
|
|
||||||
// Wait for active calls to finish
|
|
||||||
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, functions: %d, version: %d, closed: %v", len(p.scripts), len(p.functions), p.version, atomic.LoadInt32(&p.closed) == 1), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FuncList returns the list of all defined JS function names.
|
// FuncList returns the list of all defined JS function names.
|
||||||
func (p *Pool) FuncList() []string {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
list := make([]string, 0, len(p.functions))
|
|
||||||
for name := range p.functions {
|
|
||||||
list = append(list, name)
|
|
||||||
}
|
|
||||||
sort.Strings(list)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuncList() []string {
|
func FuncList() []string {
|
||||||
return DefaultPool.FuncList()
|
scriptsMu.RLock()
|
||||||
|
defer scriptsMu.RUnlock()
|
||||||
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|||||||
82
pool_test.go
82
pool_test.go
@ -2,26 +2,14 @@ package js
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPoolVersioning(t *testing.T) {
|
func TestPoolVersioning(t *testing.T) {
|
||||||
p := NewPool()
|
|
||||||
// 1. Define initial function
|
// 1. Define initial function
|
||||||
p.Define(`function hello(name) { return "Hello " + name; }`, "hello.js", int64(100))
|
Define(`function hello(name) { return "Hello " + name; }`)
|
||||||
|
|
||||||
if !p.CheckVersion("hello.js", 100) {
|
res, err := Call(context.Background(), "hello", []any{"World"})
|
||||||
t.Error("expected CheckVersion to be true for v100")
|
|
||||||
}
|
|
||||||
if p.CheckVersion("hello.js", 101) {
|
|
||||||
t.Error("expected CheckVersion to be false for v101")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := p.Call("hello", 0, nil, "World")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -30,30 +18,23 @@ func TestPoolVersioning(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Define new function (incremental update)
|
// 2. Define new function (incremental update)
|
||||||
p.Define(`function add(a, b) { return a + b; }`)
|
Define(`function add(a, b) { return a + b; }`)
|
||||||
|
|
||||||
res, err = p.Call("add", 0, nil, 1, 2)
|
res, err = Call(context.Background(), "add", []any{1, 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if cast.To[int64](res) != 3 {
|
if res.(int64) != 3 {
|
||||||
t.Errorf("expected 3, got %v", res)
|
t.Errorf("expected 3, got %v", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check FuncList
|
// 3. Ensure old function still works
|
||||||
funcs := p.FuncList()
|
res, err = Call(context.Background(), "hello", []any{"Again"})
|
||||||
foundHello := false
|
if err != nil {
|
||||||
foundAdd := false
|
t.Fatal(err)
|
||||||
for _, f := range funcs {
|
|
||||||
if f == "hello" {
|
|
||||||
foundHello = true
|
|
||||||
}
|
}
|
||||||
if f == "add" {
|
if res != "Hello Again" {
|
||||||
foundAdd = true
|
t.Errorf("expected 'Hello Again', got %v", res)
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundHello || !foundAdd {
|
|
||||||
t.Errorf("FuncList missing functions: %v", funcs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,47 +49,8 @@ func TestPoolConcurrent(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
_, _ = Call("heavy", 0, nil, 1000)
|
_, _ = Call(context.Background(), "heavy", []any{1000})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPoolGracefulShutdown(t *testing.T) {
|
|
||||||
p := NewPool()
|
|
||||||
p.Define(`function sleep(ms) {
|
|
||||||
let start = Date.now();
|
|
||||||
while(Date.now() - start < ms);
|
|
||||||
return "done";
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// 1. Test Timeout
|
|
||||||
_, err := p.Call("sleep", 100*time.Millisecond, nil, 1000)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "execution timeout/canceled") {
|
|
||||||
t.Errorf("expected timeout error, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Test Graceful Stop
|
|
||||||
go func() {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
p.Stop(context.Background())
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = p.Call("sleep", 10*time.Second, nil, 5000)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "application stopping") {
|
|
||||||
t.Errorf("expected app stopping error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlobalInjection(t *testing.T) {
|
|
||||||
p := NewPool()
|
|
||||||
// Test if 'cast' module is available globally without 'go.' prefix
|
|
||||||
p.Define(`function testGlobal() { return cast.ToJSON({a:1}); }`)
|
|
||||||
res, err := p.Call("testGlobal", 0, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res != `{"a":1}` {
|
|
||||||
t.Errorf("expected '{\"a\":1}', got %v", res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user