package jscriptx import ( "context" "errors" "fmt" "strconv" "github.com/dop251/goja" ) // goja 抛出来的东西翻译成本库的 *Error。 // // goja 的错误有四种形状(中断、栈溢出、脚本异常、其它),每种要摘的上下文都不一样, // 而调用方只想拿到「哪个脚本、哪个函数、脚本里哪一行」。这一层就干这件事。 // classify 把 goja 抛出来的各种错误翻译成 *Error 并补上脚本上下文。 // 已经是 *Error 的(比如 invoke 自己造的类型错误)原样返回。 // gojaError 是 classify 四个分支共用的骨架:脚本上下文(哪个脚本、哪个函数、 // 什么参数)四处都一样,只有 kind / msg / stack / cause 各不相同。 func gojaError(kind Kind, script, fn string, args []any, msg string, stack []Frame, cause error) *Error { return &Error{ Kind: kind, Script: script, Func: fn, Msg: msg, Args: summarize(args), Stack: stack, Cause: cause, } } func classify(err error, script, fn string, args []any) error { if err == nil { return nil } var known *Error if errors.As(err, &known) { return err } // 超时/取消:goja.Runtime.Interrupt 打断脚本后返回的就是这个。 var interrupted *goja.InterruptedError if errors.As(err, &interrupted) { cause := interrupted.Unwrap() kind, msg := KindTimeout, "脚本执行超时,已强制中断" switch { case errors.Is(cause, context.Canceled): kind, msg = KindCanceled, "调用方 context 被取消,脚本已中断" case errors.Is(cause, context.DeadlineExceeded): cause = fmt.Errorf("%w (%w)", ErrTimeout, cause) case cause == nil: cause = ErrInterrupted } return gojaError(kind, script, fn, args, msg, framesOf(interrupted.Stack()), cause) } var overflow *goja.StackOverflowError if errors.As(err, &overflow) { return gojaError(KindRuntime, script, fn, args, "脚本调用栈溢出(多半是无限递归)", framesOf(overflow.Stack()), err) } // 脚本里没被 catch 的异常。 var exception *goja.Exception if errors.As(err, &exception) { e := gojaError(KindRuntime, script, fn, args, "脚本抛出异常", framesOf(exception.Stack()), err) if v := exception.Value(); v != nil { e.Msg = v.String() e.Value = v.Export() if hint := missingGlobalHint(e.Msg); hint != "" { e.Msg += "。" + hint } } // Go 侧函数返回的 error 透到 JS 又没被 catch 时,这里能把原始 Go error 取回来, // 让调用方的 errors.Is 还能匹配到自己的哨兵错误。 if inner := exception.Unwrap(); inner != nil { e.Cause = inner } return e } return gojaError(KindRuntime, script, fn, args, err.Error(), nil, err) } func framesOf(stack []goja.StackFrame) []Frame { if len(stack) == 0 { return nil } out := make([]Frame, 0, len(stack)) for i := range stack { pos := stack[i].Position() out = append(out, Frame{ Source: stack[i].SrcName(), Func: stack[i].FuncName(), Line: pos.Line, Column: pos.Column, }) } return out } const ( maxSummaryArgs = 8 // 最多记录几个参数 maxSummaryLen = 256 // 单个参数摘要的最大长度 ) // summarize 把调用参数压成可以安全写进日志的短字符串。 // 只在出错路径上调用,正常调用不付这个格式化开销。 func summarize(args []any) []string { if len(args) == 0 { return nil } n := min(len(args), maxSummaryArgs) out := make([]string, 0, n+1) for _, a := range args[:n] { s := fmt.Sprintf("%v", a) if len(s) > maxSummaryLen { s = s[:maxSummaryLen] + "…" } out = append(out, fmt.Sprintf("%T=%s", a, s)) } if len(args) > n { out = append(out, fmt.Sprintf("…还有 %d 个参数", len(args)-n)) } return out } // fatal 判断这个错误是否说明 VM 已处于不确定状态,不该再放回池子复用。 func fatal(err error) bool { var e *Error if errors.As(err, &e) { switch e.Kind { case KindTimeout, KindCanceled, KindPanic: return true } return false } var interrupted *goja.InterruptedError if errors.As(err, &interrupted) { return true } var overflow *goja.StackOverflowError return errors.As(err, &overflow) } // toError 把 recover() 拿到的任意值转成 error。 func toError(r any) error { if err, ok := r.(error); ok { return err } return fmt.Errorf("%v", r) } // strconvQuote 是 strconv.Quote 的短名字,给错误信息拼接用。 func strconvQuote(s string) string { return strconv.Quote(s) }