fix(js): 支持可变参数中的嵌套流式回调(by AI)
This commit is contained in:
parent
7d597422ad
commit
3787accfda
@ -1,5 +1,8 @@
|
||||
# CHANGELOG - go/js
|
||||
|
||||
## v1.5.10 (2026-08-17)
|
||||
- **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。
|
||||
|
||||
## v1.5.9 (2026-07-18)
|
||||
- **依赖校验修复**: 重新整理模块 checksum,并对齐 `x/crypto v0.52.0`、`x/sys v0.45.0`。
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ A lightweight, frictionless, and AI-friendly JavaScript engine for Go applicatio
|
||||
- **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.
|
||||
- **Nested Callback Bridging**: JavaScript callbacks inside lower-camel option objects are preserved when converted to Go structs, including variadic streaming options.
|
||||
- **AI-Ready**: Generates TypeScript definitions (`.d.ts`) for AI to understand available capabilities.
|
||||
|
||||
## Usage
|
||||
|
||||
3
TEST.md
3
TEST.md
@ -1,7 +1,8 @@
|
||||
# Test Report - go/js
|
||||
|
||||
## v1.5.9 验证
|
||||
## v1.5.10 验证
|
||||
- 独立模块解析、全量测试与基准测试均通过。
|
||||
- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。
|
||||
|
||||
## Performance (Benchmark)
|
||||
Date: 2026-06-28
|
||||
|
||||
48
bridge.go
48
bridge.go
@ -89,11 +89,13 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
||||
if isVariadic && i == numIn-1 {
|
||||
elemType := argType.Elem()
|
||||
for jsArgIdx < len(jsArgs) {
|
||||
exported := jsArgs[jsArgIdx].Export()
|
||||
jsValue := jsArgs[jsArgIdx]
|
||||
exported := jsValue.Export()
|
||||
expV := reflect.ValueOf(exported)
|
||||
if !expV.IsValid() || !expV.Type().AssignableTo(elemType) {
|
||||
elem := reflect.New(elemType).Elem()
|
||||
cast.Convert(elem.Addr().Interface(), exported)
|
||||
exportJSCallbacks(vm, jsValue, elem)
|
||||
expV = elem
|
||||
}
|
||||
goArgs = append(goArgs, expV)
|
||||
@ -175,6 +177,50 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
||||
})
|
||||
}
|
||||
|
||||
// exportJSCallbacks overlays callable fields after cast.Convert has applied the
|
||||
// project's lower-camel struct mapping. goja's native ExportTo preserves JS
|
||||
// functions, while cast remains authoritative for ordinary data conversion.
|
||||
func exportJSCallbacks(vm *goja.Runtime, source goja.Value, target reflect.Value) {
|
||||
if source == nil {
|
||||
return
|
||||
}
|
||||
for target.Kind() == reflect.Ptr {
|
||||
if target.IsNil() {
|
||||
if goja.IsUndefined(source) || goja.IsNull(source) {
|
||||
return
|
||||
}
|
||||
target.Set(reflect.New(target.Type().Elem()))
|
||||
}
|
||||
target = target.Elem()
|
||||
}
|
||||
if target.Kind() != reflect.Struct || goja.IsUndefined(source) || goja.IsNull(source) {
|
||||
return
|
||||
}
|
||||
object := source.ToObject(vm)
|
||||
targetType := target.Type()
|
||||
for i := 0; i < target.NumField(); i++ {
|
||||
field := target.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
name := cast.GetLowerName(targetType.Field(i).Name)
|
||||
value := object.Get(name)
|
||||
if goja.IsUndefined(value) {
|
||||
value = object.Get(targetType.Field(i).Name)
|
||||
}
|
||||
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
|
||||
continue
|
||||
}
|
||||
if field.Kind() == reflect.Func {
|
||||
_ = vm.ExportTo(value, field.Addr().Interface())
|
||||
continue
|
||||
}
|
||||
if field.Kind() == reflect.Struct || field.Kind() == reflect.Ptr {
|
||||
exportJSCallbacks(vm, value, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toJSValue exposes Go API objects through bridge-wrapped methods rather than
|
||||
// goja's direct reflection. Directly reflected methods bypass cast.Convert, so
|
||||
// nested lower-camel JavaScript objects would not be converted to Go structs.
|
||||
|
||||
@ -24,6 +24,15 @@ type bridgeFacadeRequest struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
type bridgeCallbackGroup struct {
|
||||
OnData func([]byte) error
|
||||
}
|
||||
|
||||
type bridgeCallbackOptions struct {
|
||||
Token string
|
||||
Stream *bridgeCallbackGroup
|
||||
}
|
||||
|
||||
// bridgeFacade intentionally follows the shape used by Go APIs exposed to JS:
|
||||
// private state and exported methods.
|
||||
type bridgeFacade struct {
|
||||
@ -167,6 +176,26 @@ func TestBridgeVariadic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeVariadicNestedCallback(t *testing.T) {
|
||||
vm := goja.New()
|
||||
call := func(options ...*bridgeCallbackOptions) error {
|
||||
if len(options) != 1 || options[0] == nil || options[0].Token != "internal" || options[0].Stream == nil || options[0].Stream.OnData == nil {
|
||||
return fmt.Errorf("options were not converted: %#v", options)
|
||||
}
|
||||
return options[0].Stream.OnData([]byte("chunk"))
|
||||
}
|
||||
if err := vm.Set("call", wrapGoFunc(vm, call, false)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, err := vm.RunString(`let received = ''; call({token: 'internal', stream: {onData(chunk) { received = String.fromCharCode(...chunk); return null }}}); received`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value.String() != "chunk" {
|
||||
t.Fatalf("callback received %q", value.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeMapsLowerCamelCaseForReturnedObjectMethods(t *testing.T) {
|
||||
vm := goja.New()
|
||||
facade := &bridgeFacade{}
|
||||
|
||||
4
go.mod
4
go.mod
@ -3,9 +3,9 @@ module apigo.cc/go/js
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.5.3
|
||||
apigo.cc/go/cast v1.5.5
|
||||
apigo.cc/go/jsmod v1.5.3
|
||||
apigo.cc/go/log v1.5.8
|
||||
apigo.cc/go/log v1.5.9
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||
)
|
||||
|
||||
|
||||
4
go.sum
4
go.sum
@ -1,5 +1,7 @@
|
||||
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=
|
||||
@ -12,6 +14,8 @@ 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=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user