js

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 25, 2026 License: MIT Imports: 15 Imported by: 0

README

jsrunner

github.com/OptLTD/library/jsrunner

基于 Goja (sobek)JavaScript 运行时,在 Go 进程中嵌入 ESM/CJS 模块执行、事件循环与 Promise 支持。

用途

  • 创建与管理 JS 虚拟机(VM
  • 编译并运行 ESM/CJS 模块(CompileModuleRunModule
  • 执行脚本字符串(RunString
  • 事件循环、定时任务调度、异步 Promise
  • jsmodule 配合加载 Buffer、fetch、timers 等内置能力

主要组件

包 / 文件 职责
vm.go / js.go VM 接口与工厂
eventloop.go 事件循环
scheduler.go 任务调度
loader.go 模块加载
promise/ Promise 实现
types/ 类型辅助
modulestest/ 测试用 VM 封装

快速开始

import (
    "context"
    "time"

    js "github.com/OptLTD/library/jsrunner"
)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 执行表达式
val, err := js.RunString(ctx, `1 + 2 * 3`)

// 运行 ESM 模块
mod, err := js.CompileModule("greet", `export default (name) => "hello, " + name`)
greeting, err := js.RunModule(ctx, mod, "world")

配合内置模块:

import (
    js "github.com/OptLTD/library/jsrunner"
    _ "github.com/OptLTD/library/jsmodule/timers" // 按需 blank import
)

完整示例见 example/demo/jsrunner.go

依赖说明

jsmodule 存在循环依赖,本仓库内通过 go.workrequire + replace 联调;发版时需保持两边版本号一致,详见 deploy.md

测试

cd jsrunner && go test ./...

Documentation

Overview

Package js the JavaScript implementation.

Run ESM/CJS modules:

func main() {
	module, err := js.CompileModule("", "module.exports = () => 'some value'")
	if err != nil {
		panic(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	value, err := js.RunModule(ctx, module)
	if err != nil {
		panic(err)
	}

	fmt.Println(value.Export())
}

Index

Constants

View Source
const (
	// DefaultTimeoutGetVM default get js.VM timeout
	DefaultTimeoutGetVM = 500 * time.Millisecond
	// DefaultMaxRetriesGetVM default retries times
	DefaultMaxRetriesGetVM = 3
)

Variables

View Source
var (

	// ErrSchedulerClosed the scheduler was closed error
	ErrSchedulerClosed = errors.New("scheduler was closed")
)

Functions

func AccountFromCtx

func AccountFromCtx(ctx context.Context) any

AccountFromCtx returns the value set by WithAccount, or nil if unset.

func CallModuleExport

func CallModuleExport(ctx context.Context, module sobek.CyclicModuleRecord, exportName string, args ...any) (ret sobek.Value, err error)

CallModuleExport calls the named export function from a module.

func Cleanup

func Cleanup(rt *sobek.Runtime, job ...func())

Cleanup add a function to execute when the VM has finished running. eg: close resources...

func CompileModule

func CompileModule(name, source string) (sobek.CyclicModuleRecord, error)

CompileModule compile module from source string (cjs/esm).

func Context

func Context(rt *sobek.Runtime) context.Context

Context returns the current context of the sobek.Runtime

func EnableConsole

func EnableConsole(rt *sobek.Runtime, attr ...slog.Attr)

func Format

func Format(rt *sobek.Runtime, args ...sobek.Value) string

Format js console format

func Loader

func Loader() modules.Loader

Loader get modules.Loader

func Logger

func Logger(ctx context.Context) *slog.Logger

Logger get slog.Logger from the context

func ModuleCallable

func ModuleCallable(rt *sobek.Runtime, module sobek.CyclicModuleRecord) (sobek.Callable, error)

ModuleCallable return the sobek.CyclicModuleRecord default export as sobek.Callable.

func ModuleInstance

func ModuleInstance(rt *sobek.Runtime, module sobek.CyclicModuleRecord) (sobek.ModuleInstance, error)

ModuleInstance return the sobek.ModuleInstance.

func Register

func Register(name string, mod modules.Module)

Register registers a native module importable as "superuser/<name>" (legacy superuser-api convention).

func Run

func Run(ctx context.Context, fn func(*sobek.Runtime) error) error

Run executes the given function

example:

err := js.Run(context.Background(), func(rt *sobek.Runtime) error {
	_, err := rt.RunString(`console.log('hello world')`)
	return err
})
if err != nil {
	panic(err)
}

func RunModule

func RunModule(ctx context.Context, module sobek.CyclicModuleRecord, args ...any) (sobek.Value, error)

RunModule the sobek.CyclicModuleRecord

example:

module, err := js.CompileModule("add", "export default (a, b) => a + b")
if err != nil {
	panic(err)
}
value, err := js.RunModule(context.Background(), module, 1, 2)
if err != nil {
	panic(err)
}
fmt.Println(value.Export()) // 3

func RunProgram

func RunProgram(ctx context.Context, program *sobek.Program) (sobek.Value, error)

RunProgram executes the given sobek.Program

example:

program, err := sobek.Compile("", `1 + 1`, false)
if err != nil {
	panic(err)
}
value, err := js.RunProgram(context.Background(), program)
if err != nil {
	panic(err)
}
fmt.Println(value.Export()) // 2

func RunString

func RunString(ctx context.Context, str string) (sobek.Value, error)

RunString executes the given string

example:

value, err := js.RunString(context.Background(), `1 + 1`)
if err != nil {
	panic(err)
}
fmt.Println(value.Export()) // 2

func SetLoader

func SetLoader(ml modules.Loader)

SetLoader set the modules.Loader

func SetScheduler

func SetScheduler(s Scheduler)

SetScheduler set the default Scheduler

func Throw

func Throw(rt *sobek.Runtime, err error)

Throw js exception

func ToBytes

func ToBytes(data any) ([]byte, error)

ToBytes tries to return a byte slice from compatible types.

func Unwrap

func Unwrap(value sobek.Value) (any, error)

Unwrap the sobek.Value to the raw value

func WithAccount

func WithAccount(ctx context.Context, account any) context.Context

WithAccount attaches the current JS/storage caller identity to ctx (e.g. *request.Account from search package).

func WithLogger

func WithLogger(ctx context.Context, logger *slog.Logger) context.Context

WithLogger set the slog.Logger to context

Types

type Enqueue

type Enqueue func(func() error)

Enqueue add a job to the job queue.

func EnqueueJob

func EnqueueJob(rt *sobek.Runtime) Enqueue

EnqueueJob return a function Enqueue to add a job to the job queue.

type EventLoop

type EventLoop struct {
	// contains filtered or unexported fields
}

EventLoop implements an eventloop.

func NewEventLoop

func NewEventLoop() *EventLoop

NewEventLoop create a new EventLoop instance

func (*EventLoop) Cleanup

func (e *EventLoop) Cleanup(job ...func())

Cleanup add a function to execute when run finish.

func (*EventLoop) EnqueueJob

func (e *EventLoop) EnqueueJob() Enqueue

EnqueueJob return a function Enqueue to add a job to the job queue. Usage:

	func main() {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"foo":"bar"}`))
	}))
	defer server.Close()

	loop := js.NewEventLoop()
	runtime := sobek.New()

	_ = runtime.Set("fetch", func(call sobek.FunctionCall) sobek.Value {
		promise, resolve, reject := runtime.NewPromise()
		enqueue := loop.EnqueueJob()

		go func() {
			res, err := http.Get(call.Argument(0).String())
			if err != nil {
				enqueue(func() error { return reject(err) })
				return
			}
			loop.Cleanup(func() { res.Body.Close() })

			data, err := io.ReadAll(res.Body)
			if err != nil {
				enqueue(func() error { return reject(err) })
				return
			}

			enqueue(func() error { return resolve(string(data)) })
		}()

		return runtime.ToValue(promise)
	})

	var (
		ret sobek.Value
		err error
	)

	err = loop.Start(func() error {
		ret, err = runtime.RunString(fmt.Sprintf(`fetch("%s")`, server.URL))
		return err
	})

	if err != nil {
		panic(err)
	}
	promise, ok := ret.Export().(*sobek.Promise)
	if !ok {
		panic("expect promise")
		return
	}

	switch promise.State() {
	case sobek.PromiseStatePending:
		panic("unexpect pending state")
	case sobek.PromiseStateRejected:
		panic(promise.Result().(error))
	case sobek.PromiseStateFulfilled:
		fmt.Println(promise.Result().Export())
	}
}

func (*EventLoop) Start

func (e *EventLoop) Start(task func() error) (err error)

Start the event loop and execute the provided function

func (*EventLoop) Stop

func (e *EventLoop) Stop(err error)

Stop the eventloop with the provided error

type Global

type Global = modules.Global

Global is a namespace of lazy global modules.

type Metrics

type Metrics struct {
	Max       int `json:"max"`       // max vm size
	Idle      int `json:"idle"`      // idle vm size
	Remaining int `json:"remaining"` // remaining creatable vm size
}

Metrics contains Scheduler metrics

type Module

type Module = modules.Module

Module is the native module interface (alias for modules.Module).

type Option

type Option func(*vmImpl)

func WithInitial

func WithInitial(fn func(*sobek.Runtime)) Option

WithInitial call on VM create.

func WithRelease

func WithRelease(fn func(VM)) Option

WithRelease call on VM run finish.

type Scheduler

type Scheduler interface {
	// Get a VM from the pool (or create under cap).
	Get() (VM, error)
	// Shrink the idle VM to initial VM size
	Shrink()
	// Metrics Scheduler metrics
	Metrics() Metrics
	// Close the scheduler
	Close() error
}

Scheduler the js.VM scheduler

func GetScheduler

func GetScheduler() Scheduler

GetScheduler get the default Scheduler

func NewScheduler

func NewScheduler(opt SchedulerOptions) Scheduler

NewScheduler create a new Scheduler

type SchedulerOptions

type SchedulerOptions struct {
	InitialVMs    uint          `yaml:"initial-vms" json:"initialVMs"`
	MaxVMs        uint          `yaml:"max-vms" json:"maxVMs"`
	GetMaxRetries uint          `yaml:"get-max-retries" json:"maxRetries"`
	GetTimeout    time.Duration `yaml:"get-timeout" json:"timeout"`
	VMOptions     []Option      `yaml:"-"` // options for NewVM
}

SchedulerOptions options

type VM

type VM interface {
	// RunModule run the sobek.CyclicModuleRecord.
	// To compile the module, sobek.ParseModule or CompileModule.
	// Any additional arguments are passed to the default export function arguments.
	RunModule(ctx context.Context, module sobek.CyclicModuleRecord, args ...any) (sobek.Value, error)
	// RunString executes the given string
	RunString(ctx context.Context, str string) (sobek.Value, error)
	// RunProgram executes the given sobek.Program
	RunProgram(ctx context.Context, program *sobek.Program) (sobek.Value, error)
	// Run execute the given function in the EventLoop.
	// when context done interrupt VM execution and release the VM.
	// This is usually used when needs to be called repeatedly many times.
	// like this:
	//
	//	func main() {
	//		ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	//		defer cancel()
	//
	//		vm := js.NewVM()
	//		rt := vm.Runtime()
	//
	//		module, err := js.CompileModule("add", "export default (a, b) => a + b")
	//		if err != nil {
	//			panic(module)
	//		}
	//
	//		add, err := js.ModuleCallable(rt, module)
	//		if err != nil {
	//			panic(err)
	//		}
	//
	//		var total int64
	//		vm.Run(ctx, func() error {
	//			for i := 0; i < 8; i++ {
	//				v, err := add(sobek.Undefined(), rt.ToValue(i), rt.ToValue(total))
	//				if err != nil {
	//					panic(err)
	//				}
	//				total = v.ToInteger()
	//			}
	//			return nil
	//		})
	//
	//		fmt.Println(total)
	//	}
	Run(context.Context, func() error) error
	// Runtime return the js runtime
	Runtime() *sobek.Runtime
}

VM the js runtime. An instance of VM can only be used by a single goroutine at a time.

func NewVM

func NewVM(opts ...Option) VM

NewVM creates a new JavaScript VM Initialize the EventLoop, global module, console.

Directories

Path Synopsis
Package modulestest the module test vm
Package modulestest the module test vm

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL