From ec6280f7ab1928a621fba70481ff2fab9420b4f5 Mon Sep 17 00:00:00 2001 From: Star Date: Mon, 24 Aug 2026 12:24:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(js):=20=E9=9A=94=E7=A6=BB=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E4=BD=8E=E4=BB=A3=E7=A0=81=E5=87=BD=E6=95=B0=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=EF=BC=88by=20AI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ README.md | 3 +++ TEST.md | 15 ++++++++++----- go.mod | 16 ++++++++-------- go.sum | 36 ++++++++++++++++-------------------- pool.go | 21 ++++++++++++++++++--- pool_test.go | 29 +++++++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad8029..67c7892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG - go/js +## v1.5.11 (2026-08-24) +- **定义隔离**: `Define` 在发布到共享 VM Pool 前编译并在一次性隔离 VM 中验证脚本,拒绝语法错误、定义阶段异常、非函数结果和超过 1 秒的定义执行。 +- **故障边界**: 无效定义不再增加 Pool 版本或进入共享脚本列表,避免单个低代码脚本阻塞其他 VM 和服务。 +- **依赖对齐**: 更新 `log`、`config`、`encoding`、`file`、`id`、`rand`、`safe`、`shell` 至 monorepo 当前版本。 + ## v1.5.10 (2026-08-17) - **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。 diff --git a/README.md b/README.md index 93aea29..6895916 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A lightweight, frictionless, and AI-friendly JavaScript engine for Go applicatio - **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`. - **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`). +- **Transactional Definitions**: `Define` validates each function in an isolated VM before publishing it, so an invalid low-code script cannot poison the shared VM pool. - **Function Discovery**: List all defined functions via `FuncList()`. - **Context Interruption**: Safe execution with `context.Context` cancellation support. - **Nested Callback Bridging**: JavaScript callbacks inside lower-camel option objects are preserved when converted to Go structs, including variadic streaming options. @@ -45,6 +46,8 @@ func main() { } ``` +`Define` accepts an anonymous function expression. Before registration it rejects syntax errors, definition-time exceptions, non-function results, and definitions that do not finish within one second. Rejected definitions do not change the Pool version or become visible to any runtime VM. + ### 3. Discover Functions ```go diff --git a/TEST.md b/TEST.md index a72928f..eb6ff48 100644 --- a/TEST.md +++ b/TEST.md @@ -1,19 +1,23 @@ # Test Report - go/js +## v1.5.11 验证 +- 独立模块解析、全量测试与基准测试均通过。 +- `TestDefineValidation` 覆盖语法错误、定义阶段异常、非函数结果、定义超时,以及失败定义不改变共享注册表。 + ## v1.5.10 验证 - 独立模块解析、全量测试与基准测试均通过。 - `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。 ## Performance (Benchmark) -Date: 2026-06-28 +Date: 2026-08-24 OS: darwin -Arch: amd64 -CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +Arch: arm64 +CPU: Apple M3 Max | Benchmark | Iterations | Time/op | |-----------|------------|---------| -| BenchmarkCall | 766462 | 1331 ns/op | -| BenchmarkSync | 31066 | 52789 ns/op | +| BenchmarkCall | 1578303 | 742.8 ns/op | +| BenchmarkSync | 13456 | 136636 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 VM sync).* @@ -66,6 +70,7 @@ ok apigo.cc/go/js 0.800s - [x] Context and Logger injection. - [x] Concurrent execution and script versioning. - [x] Script version checking (`CheckVersion`). +- [x] Transactional definition validation and shared Pool fault isolation. - [x] Function discovery (`FuncList`). - [x] Context cancellation interruption. - [x] Graceful shutdown. diff --git a/go.mod b/go.mod index 5c7c7b3..2860009 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,18 @@ go 1.25.0 require ( apigo.cc/go/cast v1.5.5 apigo.cc/go/jsmod v1.5.3 - apigo.cc/go/log v1.5.9 + apigo.cc/go/log v1.5.11 github.com/dop251/goja v0.0.0-20260311135729-065cd970411c ) require ( - apigo.cc/go/config v1.5.3 // indirect - apigo.cc/go/encoding v1.5.5 // indirect - apigo.cc/go/file v1.5.5 // indirect - apigo.cc/go/id v1.5.6 // indirect - apigo.cc/go/rand v1.5.3 // indirect - apigo.cc/go/safe v1.5.2 // indirect - apigo.cc/go/shell v1.5.4 // indirect + apigo.cc/go/config v1.5.4 // indirect + apigo.cc/go/encoding v1.5.6 // indirect + apigo.cc/go/file v1.5.6 // indirect + apigo.cc/go/id v1.5.7 // indirect + apigo.cc/go/rand v1.5.4 // indirect + apigo.cc/go/safe v1.5.3 // indirect + apigo.cc/go/shell v1.5.6 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect diff --git a/go.sum b/go.sum index 9eaa316..11c946d 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,23 @@ -apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw= -apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw= apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= -apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A= -apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ= -apigo.cc/go/encoding v1.5.5 h1:kduNLWQgtcQqHYobOuu1djbgg8LedkGOe8f18ZMfqzs= -apigo.cc/go/encoding v1.5.5/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8= -apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs= -apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo= -apigo.cc/go/id v1.5.6 h1:Z3PPp8H8FgNHMwHerxtT2GgstueDv4aCDnUUCoQZoFQ= -apigo.cc/go/id v1.5.6/go.mod h1:HcJK691qfBPzvQ/lt8bo/incKUFtG5vENX/rDEgzJh0= +apigo.cc/go/config v1.5.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M= +apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0= +apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M= +apigo.cc/go/encoding v1.5.6/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8= +apigo.cc/go/file v1.5.6 h1:Y7w3Tyu4e16VuED7rF2pzba+dzGE+hjDnBlIPVHIfzA= +apigo.cc/go/file v1.5.6/go.mod h1:9sdW4ylSOA0HWc8Yt8qdnmMf6nn5SUEmjoPKyXpYXIQ= +apigo.cc/go/id v1.5.7 h1:Y5Sx6sQBCAdYMCQPTjODZyGMMd1+WRWCy2dWHVq11XQ= +apigo.cc/go/id v1.5.7/go.mod h1:fugudFBqfVNakfm91zZzuzU0P4PULzo6sylB8hRMqxA= apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU= apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= -apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0= -apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE= -apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc= -apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs= -apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius= -apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= -apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg= -apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw= -apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc= -apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY= +apigo.cc/go/log v1.5.11 h1:r7vHkzpdelggNguZZK4e3O9bcaxfQWjXkxsKYw4A8Bo= +apigo.cc/go/log v1.5.11/go.mod h1:C6qtOn09miyCK7FXEAAwEZASnBow76K7GZFoBaq9eiU= +apigo.cc/go/rand v1.5.4 h1:eessFBsKQuoOYdzrStldOGw9f4HqbeO87X95bI4jIBQ= +apigo.cc/go/rand v1.5.4/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= +apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs= +apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI= +apigo.cc/go/shell v1.5.6 h1:2i93lNJ0oy/D5kOmlmOjwVjLPsW7vQBo0trLOF9AAAI= +apigo.cc/go/shell v1.5.6/go.mod h1:Bp73DGKESOISWSIGUtL1dsFo5g4SI10GhgsnLJCTpsw= 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= diff --git a/pool.go b/pool.go index e058301..79bc326 100644 --- a/pool.go +++ b/pool.go @@ -127,7 +127,21 @@ func (p *Pool) Define(name string, code string, version int64) error { return err } - wrapped := fmt.Sprintf("globalThis['%s'] = (%s);", name, code) + wrapped := fmt.Sprintf("globalThis[%q] = (%s);", name, code) + program, err := goja.Compile(name, wrapped, false) + if err != nil { + return fmt.Errorf("js.Define [%s]: invalid JavaScript: %w", name, err) + } + validationVM := goja.New() + validationTimer := time.AfterFunc(time.Second, func() { validationVM.Interrupt("definition validation timed out") }) + if _, err = validationVM.RunProgram(program); err != nil { + validationTimer.Stop() + return fmt.Errorf("js.Define [%s]: definition failed: %w", name, err) + } + validationTimer.Stop() + if _, ok := goja.AssertFunction(validationVM.Get(name)); !ok { + return fmt.Errorf("js.Define [%s]: code must evaluate to a function", name) + } p.mu.Lock() defer p.mu.Unlock() @@ -164,8 +178,9 @@ func CheckVersion(name string, version int64) bool { // parseJSFrame parses a single stack trace line from Goja. // Format is: -// named: \tat funcName (src:line:col) (optionalPC) -// anon: \tat src:line:col (optionalPC) +// +// named: \tat funcName (src:line:col) (optionalPC) +// anon: \tat src:line:col (optionalPC) func parseJSFrame(line string) (src, lineNum, col string, ok bool) { if !strings.HasPrefix(line, "\t") { return "", "", "", false diff --git a/pool_test.go b/pool_test.go index 2654edc..308302b 100644 --- a/pool_test.go +++ b/pool_test.go @@ -161,6 +161,35 @@ func TestDefineValidation(t *testing.T) { if err != nil { t.Errorf("unexpected error for arrow function: %v", err) } + + // Invalid JavaScript must not enter the shared registry or poison VMs. + version := p.version + err = p.Define("badSyntax", `({ args }) => { return args })`, 0) + if err == nil || !strings.Contains(err.Error(), "invalid JavaScript") { + t.Fatalf("expected syntax validation error, got %v", err) + } + if p.version != version || p.CheckVersion("badSyntax", 0) { + t.Fatal("invalid JavaScript changed the shared script registry") + } + err = p.Define("throwsWhileDefining", `(() => { throw new Error("definition failed") })()`, 0) + if err == nil || !strings.Contains(err.Error(), "definition failed") { + t.Fatalf("expected definition error, got %v", err) + } + err = p.Define("notAFunction", `({ value: 1 })`, 0) + if err == nil || !strings.Contains(err.Error(), "must evaluate to a function") { + t.Fatalf("expected function validation error, got %v", err) + } + err = p.Define("neverFinishes", `(() => { for (;;) {} })()`, 0) + if err == nil || !strings.Contains(err.Error(), "definition validation timed out") { + t.Fatalf("expected definition timeout, got %v", err) + } + if p.version != version || p.CheckVersion("throwsWhileDefining", 0) || p.CheckVersion("notAFunction", 0) || p.CheckVersion("neverFinishes", 0) { + t.Fatal("failed definitions changed the shared script registry") + } + result, callErr := p.Call("good2", 0, nil, 2, 3) + if callErr != nil || cast.To[int64](result) != 5 { + t.Fatalf("valid function failed after rejected script: result=%v error=%v", result, callErr) + } } func TestJSErrorStackTrace(t *testing.T) {