feat: align versioned pool, CheckVersion and FuncList (by AI)
This commit is contained in:
parent
a2089b0c17
commit
002bcf0cf7
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,5 +1,18 @@
|
|||||||
# CHANGELOG - go/js
|
# 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)
|
## v1.5.1 (2026-06-05)
|
||||||
- **架构重构: 多例支持与优雅停机**:
|
- **架构重构: 多例支持与优雅停机**:
|
||||||
- 引入 `Pool` 结构体,支持通过 `js.NewPool()` 创建相互隔离的执行环境,避免业务间脚本冲突。
|
- 引入 `Pool` 结构体,支持通过 `js.NewPool()` 创建相互隔离的执行环境,避免业务间脚本冲突。
|
||||||
|
|||||||
21
README.md
21
README.md
@ -8,7 +8,9 @@ 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.
|
- **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`).
|
||||||
|
- **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
|
||||||
@ -27,23 +29,28 @@ func init() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Execute JS
|
### 2. Execute JS with Version Checking
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "apigo.cc/go/js"
|
import "apigo.cc/go/js"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
js.Define(`
|
// Check if script needs update (e.g., from file mtime)
|
||||||
function myTask(name) {
|
if !js.CheckVersion("myTask.js", mtime) {
|
||||||
let data = go.db.query("SELECT * FROM users WHERE name = ?", [name]);
|
js.Define(code, "myTask.js", mtime)
|
||||||
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
Normal file
45
TEST.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 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.
|
||||||
35
bench_test.go
Normal file
35
bench_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkCall(b *testing.B) {
|
||||||
|
p := NewPool()
|
||||||
|
p.Define(`function add(a, b) { return a + b; }`)
|
||||||
|
ctx := context.Background()
|
||||||
|
args := []any{1, 2}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := p.Call(ctx, "add", 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(context.Background(), "f", nil)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
go.mod
11
go.mod
@ -5,12 +5,23 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.5.0
|
apigo.cc/go/cast v1.5.0
|
||||||
apigo.cc/go/jsmod v1.5.0
|
apigo.cc/go/jsmod v1.5.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
|
||||||
)
|
)
|
||||||
|
|||||||
31
go.sum
31
go.sum
@ -1,7 +1,23 @@
|
|||||||
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||||
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||||
|
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 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||||
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
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/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=
|
||||||
@ -12,7 +28,22 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
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/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=
|
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.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=
|
||||||
|
|||||||
106
pool.go
106
pool.go
@ -3,6 +3,9 @@ package js
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@ -11,6 +14,12 @@ import (
|
|||||||
"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
|
||||||
@ -19,7 +28,9 @@ type vmInstance struct {
|
|||||||
// Pool represents an isolated JS execution environment with its own script registry and VM pool.
|
// Pool represents an isolated JS execution environment with its own script registry and VM pool.
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
version int32
|
version int32
|
||||||
scripts []string
|
scripts []*scriptEntry
|
||||||
|
scriptMap map[string]*scriptEntry
|
||||||
|
functions map[string]struct{}
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
pool sync.Pool
|
pool sync.Pool
|
||||||
|
|
||||||
@ -32,7 +43,10 @@ type Pool struct {
|
|||||||
|
|
||||||
// NewPool creates a new isolated JS execution environment.
|
// NewPool creates a new isolated JS execution environment.
|
||||||
func NewPool() *Pool {
|
func NewPool() *Pool {
|
||||||
p := &Pool{}
|
p := &Pool{
|
||||||
|
scriptMap: make(map[string]*scriptEntry),
|
||||||
|
functions: make(map[string]struct{}),
|
||||||
|
}
|
||||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||||
p.pool = sync.Pool{
|
p.pool = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
@ -60,7 +74,7 @@ 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 reflectType := fmt.Sprintf("%T", val); reflectType == "func" || (len(reflectType) > 4 && reflectType[:4] == "func") {
|
if val != nil && reflect.TypeOf(val).Kind() == reflect.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))
|
||||||
@ -72,15 +86,65 @@ func createNewRuntime() *goja.Runtime {
|
|||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define adds JS code to the pool's registry and increments the version.
|
var funcRegex = regexp.MustCompile(`function\s+([a-zA-Z0-9_]+)\s*\(`)
|
||||||
func (p *Pool) Define(code string) {
|
var constFuncRegex = regexp.MustCompile(`(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*(?:function|\([^)]*\)\s*=>)`)
|
||||||
|
|
||||||
|
// Define adds JS code to the pool's registry.
|
||||||
|
// name and version are optional and used for CheckVersion.
|
||||||
|
func (p *Pool) Define(code string, args ...any) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
p.scripts = append(p.scripts, code)
|
name := ""
|
||||||
|
version := int64(0)
|
||||||
|
if len(args) > 0 {
|
||||||
|
if s, ok := args[0].(string); ok {
|
||||||
|
name = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
if v, ok := args[1].(int64); ok {
|
||||||
|
version = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &scriptEntry{name: name, code: code, version: version}
|
||||||
|
p.scripts = append(p.scripts, entry)
|
||||||
|
if name != "" {
|
||||||
|
p.scriptMap[name] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
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.
|
||||||
func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...CallOption) (any, error) {
|
||||||
if atomic.LoadInt32(&p.closed) == 1 {
|
if atomic.LoadInt32(&p.closed) == 1 {
|
||||||
@ -95,13 +159,14 @@ func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...Ca
|
|||||||
defer p.wg.Done()
|
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)
|
currentVersion := atomic.LoadInt32(&p.version)
|
||||||
if instance.version < currentVersion {
|
if instance.version < currentVersion {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
for i := int(instance.version); i < len(p.scripts); i++ {
|
for i := int(instance.version); i < len(p.scripts); i++ {
|
||||||
_, err := vm.RunString(p.scripts[i])
|
_, err := vm.RunString(p.scripts[i].code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.mu.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)
|
||||||
@ -116,6 +181,19 @@ func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...Ca
|
|||||||
_ = vm.Set("__safeMode__", true) // Default is safe
|
_ = vm.Set("__safeMode__", true) // Default is safe
|
||||||
_ = vm.Set("__logger__", goja.Undefined())
|
_ = vm.Set("__logger__", goja.Undefined())
|
||||||
|
|
||||||
|
// Set up context interruption
|
||||||
|
if ctx != nil && ctx.Done() != nil {
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
vm.Interrupt("context canceled")
|
||||||
|
case <-stop:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Apply Options
|
// Apply Options
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(vm)
|
opt(vm)
|
||||||
@ -158,8 +236,8 @@ func (p *Pool) Call(ctx context.Context, funcName string, args []any, opts ...Ca
|
|||||||
|
|
||||||
// --- Global Proxy Functions ---
|
// --- Global Proxy Functions ---
|
||||||
|
|
||||||
func Define(code string) {
|
func Define(code string, args ...any) {
|
||||||
DefaultPool.Define(code)
|
DefaultPool.Define(code, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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) {
|
||||||
@ -200,7 +278,7 @@ func (p *Pool) Stop(ctx context.Context) error {
|
|||||||
func (p *Pool) Status() (string, error) {
|
func (p *Pool) Status() (string, error) {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
return fmt.Sprintf("scripts: %d, version: %d, closed: %v", len(p.scripts), p.version, atomic.LoadInt32(&p.closed) == 1), nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper types from original file ---
|
// --- Helper types from original file ---
|
||||||
@ -226,8 +304,12 @@ func WithLogger(logger *log.Logger) CallOption {
|
|||||||
func (p *Pool) FuncList() []string {
|
func (p *Pool) FuncList() []string {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
// Reflection to list functions in the latest script set could be added here
|
list := make([]string, 0, len(p.functions))
|
||||||
return []string{}
|
for name := range p.functions {
|
||||||
|
list = append(list, name)
|
||||||
|
}
|
||||||
|
sort.Strings(list)
|
||||||
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuncList() []string {
|
func FuncList() []string {
|
||||||
|
|||||||
39
pool_test.go
39
pool_test.go
@ -3,13 +3,23 @@ package js
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPoolVersioning(t *testing.T) {
|
func TestPoolVersioning(t *testing.T) {
|
||||||
|
p := NewPool()
|
||||||
// 1. Define initial function
|
// 1. Define initial function
|
||||||
Define(`function hello(name) { return "Hello " + name; }`)
|
p.Define(`function hello(name) { return "Hello " + name; }`, "hello.js", int64(100))
|
||||||
|
|
||||||
res, err := Call(context.Background(), "hello", []any{"World"})
|
if !p.CheckVersion("hello.js", 100) {
|
||||||
|
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(context.Background(), "hello", []any{"World"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -18,23 +28,30 @@ func TestPoolVersioning(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Define new function (incremental update)
|
// 2. Define new function (incremental update)
|
||||||
Define(`function add(a, b) { return a + b; }`)
|
p.Define(`function add(a, b) { return a + b; }`)
|
||||||
|
|
||||||
res, err = Call(context.Background(), "add", []any{1, 2})
|
res, err = p.Call(context.Background(), "add", []any{1, 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if res.(int64) != 3 {
|
if cast.To[int64](res) != 3 {
|
||||||
t.Errorf("expected 3, got %v", res)
|
t.Errorf("expected 3, got %v", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ensure old function still works
|
// 3. Check FuncList
|
||||||
res, err = Call(context.Background(), "hello", []any{"Again"})
|
funcs := p.FuncList()
|
||||||
if err != nil {
|
foundHello := false
|
||||||
t.Fatal(err)
|
foundAdd := false
|
||||||
|
for _, f := range funcs {
|
||||||
|
if f == "hello" {
|
||||||
|
foundHello = true
|
||||||
}
|
}
|
||||||
if res != "Hello Again" {
|
if f == "add" {
|
||||||
t.Errorf("expected 'Hello Again', got %v", res)
|
foundAdd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHello || !foundAdd {
|
||||||
|
t.Errorf("FuncList missing functions: %v", funcs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user