moejs

package module
v0.1.0-alpha.6 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

moejs

moejs

A JavaScript runtime in pure Go, for running JavaScript plugins inside Go programs.

简体中文 | English

moejs supports modern JavaScript, including ES modules, classes, async/await, Proxy and BigInt, and passes all 79,385 test262 tests it runs.

On the same plugins as Sobek, another pure-Go engine, a moejs call takes about half the time and a runtime uses about a third of the memory, so a server can give every concurrent request its own runtime.

Performance

The chart runs new-api's plugin workloads on the engines a Go program can embed: small task-plugin calls, 100k-token coding-agent requests that a plugin rewrites for the upstream, and requests that carry an 8 MiB image. QuickJS called straight from C is shown in grey for reference. How it was measured is in docs/performance.md.

The table below times single calls on new-api's 10 task plugins and 269 recorded calls, on the Go caller's side, including converting the arguments and the result.

moejs Sobek QuickJS (quickjs-go, default settings) V8 (v8go)
One plugin call 6.9 µs 14.3 µs 104.5 µs 56.6 µs ¹
New runtime 1.4 µs 2.2 µs 382 µs 1,153 µs ¹
Memory per runtime with the largest plugin loaded 81 KiB 264 KiB 348 KiB ² 1,544 KiB ²

¹ V8's timings varied widely on the test machine. ² The engine's own heap.

Sobek is a pure-Go engine like moejs. QuickJS and V8 run through cgo. The test machine, the full results and how to reproduce them are in docs/performance.md.

Features

Pure Go

moejs builds with CGO_ENABLED=0 and cross-compiles without a C toolchain. The engine is ordinary Go code, so pprof and the race detector see inside it.

Passing values

Plugin functions take Go maps, slices, structs and named types, or JSON. moejs converts a map one level at a time, as far as the plugin reads it, and JSON text in a json.RawMessage the same way. Results come back as Go values or JSON, or go straight into your own struct with the same result as json.Unmarshal. A json.RawMessage field receives the JSON text of its part of the result, and DecodeOptions caps the size of a result before anything is decoded. Host functions are plain Go functions, and one can return a promise and settle it later from Go.

What a plugin can reach

A plugin sees the JavaScript standard library and the globals you install. Every import and import() goes through a Go function you provide, and eval and new Function can be capped in length or turned off. Builtins are frozen and shared by every runtime, so every plugin sees the same Array.prototype. Each runtime has its own globals and time zone, and a host can give a runtime its own mutable copy of the builtins for plugins that patch them.

TypeScript

CompileTS runs a TypeScript module the way Node strips types: the parser drops the type syntax, so errors and stack traces point into the TypeScript source. Enums, namespaces with values and other TypeScript that changes what the code does at run time fail with a *SyntaxError.

Timers and the event loop

A bare runtime has no timers. The eventloop package adds setTimeout, setInterval and setImmediate, and an event loop on which host functions settle promises from other goroutines.

Timeouts and errors

Any goroutine can interrupt a running plugin, even one stuck in an endless loop. A JavaScript throw, a syntax error, an interrupt and a panic in a host function each come back as their own Go error type, and a thrown Error has a V8-style stack trace. A runtime stays usable after a host function panics.

Memory limit

Options.MemoryLimit caps what one request's JavaScript allocates. A plugin that goes past it stops with an error that carries its JavaScript stack, and the script cannot catch it. Stats reports a runtime's allocations and other counters.

Quick start

go get github.com/Calcium-Ion/moejs

moejs needs Go 1.25 or later and depends only on the standard library (tests use testify).

package main

import (
	"context"
	"errors"
	"fmt"
	"runtime"
	"sync"
	"time"

	"github.com/Calcium-Ion/moejs"
)

const source = `
export function buildRequest(task) {
  return {
    method: "POST",
    url: "https://api.example.com/v1/tasks",
    headers: { authorization: "Bearer " + utils.env("API_KEY") },
    body: { prompt: task.prompt.trim(), n: task.n ?? 1 },
  };
}
export function spin() { for (;;) {} }
`

// Request is what buildRequest returns.
type Request struct {
	Method  string            `json:"method"`
	URL     string            `json:"url"`
	Headers map[string]string `json:"headers"`
	Body    struct {
		Prompt string `json:"prompt"`
		N      int    `json:"n"`
	} `json:"body"`
}

// Plugin is a compiled plugin and a pool of runtimes that have loaded it.
type Plugin struct {
	mod  *moejs.Module
	idle chan *moejs.Runtime
}

func NewPlugin(name, source string, size int) (*Plugin, error) {
	// Compile once. Every runtime in the pool loads the same Module.
	mod, err := moejs.Compile(name, source)
	if err != nil {
		return nil, err
	}
	return &Plugin{mod: mod, idle: make(chan *moejs.Runtime, size)}, nil
}

// Call runs hook with body, a JSON text such as a request body, on a runtime
// from the pool and decodes the result into out. Any number of goroutines can
// call it at once.
func (p *Plugin) Call(ctx context.Context, hook moejs.Hook, body string, out any) error {
	rt, err := p.get()
	if err != nil {
		return err
	}
	defer p.put(rt)

	// Interrupt stops the hook when ctx ends. Any goroutine can call it.
	interrupted := make(chan struct{})
	stop := context.AfterFunc(ctx, func() {
		rt.Interrupt(context.Cause(ctx))
		close(interrupted)
	})
	defer func() {
		if !stop() {
			<-interrupted // Interrupt must return before rt goes back to the pool.
		}
	}()

	// ParseJSONString parses the text without copying it: strings in the
	// arguments share body's memory.
	arg, err := rt.ParseJSONString(body)
	if err != nil {
		return err
	}
	res, err := rt.Call(hook, arg)
	if err != nil {
		return err
	}
	// Unmarshal writes the result straight into out, with no JSON text in between.
	return rt.Unmarshal(res, out)
}

// get takes an idle runtime or makes a new one: host functions first, then the module.
func (p *Plugin) get() (*moejs.Runtime, error) {
	select {
	case rt := <-p.idle:
		return rt, nil
	default:
	}
	rt := moejs.NewRuntime(moejs.Options{})
	if err := rt.SetGlobal("utils", map[string]any{"env": moejs.NativeFunc(env)}); err != nil {
		return nil, err
	}
	if err := rt.Load(p.mod); err != nil {
		return nil, err
	}
	return rt, nil
}

// put resets the runtime and returns it to the pool.
func (p *Plugin) put(rt *moejs.Runtime) {
	rt.ClearInterrupt()
	rt.ReleaseCallData() // The idle runtime lets go of this call's arguments.
	select {
	case p.idle <- rt:
	default: // The pool is full.
	}
}

var secrets = map[string]string{"API_KEY": "test-key"}

// env is the host function behind utils.env.
func env(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
	name, err := r.ToString(moejs.Arg(args, 0))
	if err != nil {
		return moejs.Undefined(), err
	}
	v, ok := secrets[name.GoString()]
	if !ok {
		// A Go error becomes a JavaScript Error with this message.
		return moejs.Undefined(), fmt.Errorf("%s is not set", name.GoString())
	}
	return moejs.String(v), nil
}

func main() {
	p, err := NewPlugin("plugin.js", source, runtime.GOMAXPROCS(0))
	if err != nil {
		panic(err)
	}
	// Look hooks up once and reuse them on every call.
	build, err := p.mod.Hook("buildRequest")
	if err != nil {
		panic(err)
	}
	spin, err := p.mod.Hook("spin")
	if err != nil {
		panic(err)
	}

	// Concurrent calls each get their own runtime.
	reqs := make([]Request, 3)
	var wg sync.WaitGroup
	for i := range reqs {
		wg.Go(func() {
			body := fmt.Sprintf(`{"prompt": " cat %d ", "n": %d}`, i, i+1)
			if err := p.Call(context.Background(), build, body, &reqs[i]); err != nil {
				panic(err)
			}
		})
	}
	wg.Wait()
	for _, req := range reqs {
		fmt.Println(req.Method, req.URL, req.Headers["authorization"], req.Body.Prompt, req.Body.N)
	}
	// POST https://api.example.com/v1/tasks Bearer test-key cat 0 1
	// POST https://api.example.com/v1/tasks Bearer test-key cat 1 2
	// POST https://api.example.com/v1/tasks Bearer test-key cat 2 3

	// A JavaScript throw comes back as *moejs.Exception.
	err = p.Call(context.Background(), build, "{}", &Request{})
	var exc *moejs.Exception
	fmt.Println(errors.As(err, &exc), exc.Name(), exc.Message())
	// true TypeError Cannot read properties of undefined (reading 'trim')

	// A hook still running when ctx ends stops with *moejs.InterruptedError.
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	err = p.Call(ctx, spin, "null", nil)
	var interrupted *moejs.InterruptedError
	fmt.Println(errors.As(err, &interrupted), interrupted.Value)
	// true context deadline exceeded
}

Call takes its arguments as JSON text because that is how a request usually brings them, and ParseJSONString hands the text to the plugin without copying it. This is the "moejs (no copy)" path in the chart above. An HTTP handler can read the body into a strings.Builder, growing it to r.ContentLength first when that is known, and pass b.String(), which shares the builder's buffer. ParseJSON takes a []byte and copies it once, and FromGo takes Go maps, slices and structs.

A server calling plugins the way Plugin does pays the least per request. Taking a runtime from the pool is one channel receive, while a new runtime with the largest plugin loaded takes about 71 µs. The guide covers pools, module graphs, TypeScript, value conversion, promises, errors and the event loop, and the package documentation describes every function.

moejs ships a default.pgo profile recorded from the plugin workload. Go applies a profile automatically only from the main package's directory, so pass it when you build:

go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" .

JavaScript support

moejs runs ES modules and classic scripts, including sloppy mode and the web-compatibility behaviour of Annex B. The language includes classes with fields, private members and static blocks, destructuring, optional chaining, generators, async functions and async iteration. The standard library has Proxy, Reflect, BigInt, typed arrays, resizable ArrayBuffers, WeakRef, structuredClone, TextEncoder/TextDecoder, and recent additions such as the Set methods, Promise.try, Float16Array, Array.fromAsync, Math.sumPrecise and Error.isError. Regular expressions support every flag and Unicode 17 properties, and Date takes its time zones from Go.

On test262, 79,385 tests pass and 0 fail. The other 14,058 tests use features moejs does not implement and are skipped. Results by directory are in bench/test262/RESULTS.md.

Not implemented: import attributes and JSON modules, using declarations and DisposableStack, decorators, iterator helpers, Intl, Temporal, ShadowRealm, FinalizationRegistry and JSON.rawJSON. A bare Runtime has no timers; the eventloop package installs them. TODO.md lists all of these, the known wrong results and the limits.

Status

moejs is in alpha, and the API may change between releases.

Testing

# Test inputs are downloaded separately: new-api's plugins, a pinned
# test262 revision and pinned TypeScript sources (pi-mono, TypeScript,
# TypeBox). Tests that need them skip until they are downloaded.
bench/testdata/plugins/fetch.sh
bench/test262/fetch.sh
bench/tscorpus/fetch.sh

# Unit, audit and fuzz-corpus tests of the engine.
go test ./...

# Differential tests against Sobek and esbuild, the expression corpus, the
# benchmarks and test262. bench/ is a separate Go module, so Sobek and the
# cgo engines are dependencies of bench/ only. Its V8 and QuickJS baselines
# need cgo.
cd bench && go test -timeout 30m ./...

Acknowledgements

moejs takes design ideas from these projects. Its code was written independently.

  • goja and Sobek: the Go interop conventions and the Export rules. Sobek is also the reference for the differential tests.
  • QuickJS: the 16-byte value layout, atoms, shape transitions and compact builtin tables.
  • V8: hidden classes, inline caches, prototype validity checks (reduced to one counter), the Ignition register interpreter, Date.parse and the Error.prototype.stack format.
  • Lua 5.x: the fixed-width register instruction encoding.
  • JavaScriptCore and SpiderMonkey: NaN-boxing.
  • TypeScript: the grammar and the disambiguation rules that CompileTS follows.
  • esbuild: how to write a fast JavaScript parser in Go.
  • Hardened JavaScript / SES: the lockdown() model behind the shared frozen builtins.
  • quickjs-go and v8go: the cgo baselines of the benchmarks.
  • test262: the conformance suite.
  • new-api: the plugin host. Its pkg/jsplugin decides which APIs moejs has to support.

License

moejs is licensed under the Apache License 2.0.

The benchmarks run new-api's task plugins, which are licensed under AGPL-3.0 and downloaded separately by bench/testdata/plugins/fetch.sh.

Documentation

Overview

Package moejs embeds the moejs JavaScript engine in a Go host that runs ES modules as plugins: compile a module once, evaluate it in any number of runtimes, call its exported functions with Go data and read their results back as Go data or JSON bytes.

mod, err := moejs.Compile("plugin.js", source)
hook, err := mod.Hook("protocols", "openai", "decodeRequest")
rt := moejs.NewRuntime(moejs.Options{})
err = rt.SetGlobal("utils", map[string]any{"now": moejs.NativeFunc(now)})
err = rt.Load(mod)
arg, err := rt.ParseJSON(body)
res, err := rt.Call(hook, arg)
out, err := rt.AppendJSON(nil, res)

Values are engine values (package engine) under their own names, so host functions and the host share one representation and nothing is wrapped on the way in or out. Errors are returned, never panicked: a JavaScript throw is an *Exception, an interrupt an *InterruptedError, bad source a *SyntaxError.

The jobs JavaScript queues (promise reactions, queueMicrotask callbacks) run before the method that ran it returns, getters and proxy traps included (Load, Call, CallFunction, Has, Get, ToGo, AppendJSON, SetGlobal, the settlers of NewPromise): the first exception a callback throws is the method's error when it succeeded otherwise, and an interrupt drops the jobs left.

A module that imports others is linked once with the modules it imports, which the host resolves (moejs never reads files or the network):

mod, err := moejs.Link(entry, func(referrer moejs.Referrer, specifier string) (*moejs.Module, error) {
	return host.modules[specifier], nil // compiled once, by the host
})

A runtime that loads the linked module evaluates each module of its graph once; its hooks and exports are the entry's, re-exported names included. import() and import.meta are the host's too, through the Importer of Options, which resolves with the same kind of resolver:

rt := moejs.NewRuntime(moejs.Options{Importer: &moejs.Importer{Resolve: resolve}})

A module import() loads joins the runtime's modules: one the runtime evaluated before is not evaluated again.

A Module is immutable and may be loaded by many runtimes concurrently. A Runtime is one global environment with one loaded module; it must be used from one goroutine at a time, except Interrupt and ClearInterrupt.

Index

Examples

Constants

View Source
const (
	PromisePending   = engine.PromisePending
	PromiseFulfilled = engine.PromiseFulfilled
	PromiseRejected  = engine.PromiseRejected
)

Promise states.

View Source
const (
	// PromiseRejectionReject: the promise was rejected with no handler.
	PromiseRejectionReject = engine.PromiseRejectionReject
	// PromiseRejectionHandle: the first handler was added to the promise,
	// rejected earlier with none.
	PromiseRejectionHandle = engine.PromiseRejectionHandle
)

Rejection tracker operations.

Variables

View Source
var (
	Undefined = engine.Undefined
	Null      = engine.Null
	Bool      = engine.Bool
	Number    = engine.NumberValue
	Int       = engine.Int64Value
)

Value constructors, for host functions. None allocates except String.

View Source
var (
	// ErrHookNotFound is returned when a hook's export or one of its members
	// is missing, undefined or null.
	ErrHookNotFound = errors.New("moejs: hook not found")
	// ErrNotCallable is returned when a hook names a value that is not a
	// function, and when CallFunction is given one.
	ErrNotCallable = errors.New("moejs: not a function")
	// ErrForeign is returned for a function or generator of another
	// runtime (or a bound function or proxy of one) passed to Call,
	// CallFunction, SetGlobal, FromGo or a settler of NewPromise: it runs
	// only in the runtime that created it. Inside a Go map or slice FromGo
	// or SetGlobal converts it is not the error: reading that member throws
	// a TypeError with ErrForeign's text.
	ErrForeign = engine.ErrForeign
	// ErrModulePending is Load's error for a module with top-level await
	// whose evaluation still awaits once no job is left.
	ErrModulePending = engine.ErrModulePending
	// ErrMemoryLimit is what errors.Is finds in the error of a call that
	// passed Options.MemoryLimit (a *MemoryLimitError).
	ErrMemoryLimit = engine.ErrMemoryLimit
)
View Source
var Arg = engine.Arg

Arg returns args[i], or undefined when there are fewer arguments.

View Source
var ErrNoResolver = engine.ErrNoResolver

ErrNoResolver is the Err of the *ResolveError of Link for a module that imports when the resolver is nil.

View Source
var ErrTooLarge = engine.ErrTooLarge

ErrTooLarge is the error, wrapped in one that names the limit, of a decode whose JSON text passes one of its DecodeOptions limits. A value nested deeper than 10,000 arrays and objects, a cycle included, passes them all.

Functions

func PromiseResult

func PromiseResult(p Value) (state PromiseState, result Value, ok bool)

PromiseResult reports the state of the promise p and its result: the fulfillment value or the rejection reason, undefined while pending. ok is false when p is not a promise. No user code runs, so a promise a Call returned is read as the jobs that Call ran left it.

Types

type DecodeOptions

type DecodeOptions = engine.DecodeOptions

DecodeOptions bounds the JSON text of a decode of a JavaScript value into Go (UnmarshalWith, ToGoIntoWith, ToGoWith): MaxBytes its length in bytes, MaxNodes the values in it (each object, array, string, number, boolean and null; keys do not count). The text is the one the decode's round trip writes: AppendJSON's text of the value for UnmarshalWith, json.Marshal's text of what ToGo returns for ToGoIntoWith and ToGoWith, counted exactly, whether or not the decode writes it. A zero field sets no limit.

type Exception

type Exception = engine.Exception

Exception is a thrown JavaScript value.

type Hook

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

Hook is a resolved path to a function in a module (Module.Hook), which Call finds in a runtime that loaded the module. A Hook of an export the module declares itself also works in a runtime that loaded the Module Link returned for it; a re-exported name needs the Hook of that Module. The zero Hook names nothing.

func (Hook) Name

func (h Hook) Name() string

Name returns the path joined with dots ("protocols.openai.decodeRequest").

type Importer

type Importer struct {
	// Resolve returns the module import(specifier) names in the code of
	// referrer: the *Module whose code imports (Link's entry or a module a
	// resolver returned for its graph; Compile's module for a module Load
	// evaluated alone), or the *Script RunScript ran; nil for code of
	// neither. Eval code and the code of the Function constructors import
	// for the script or module whose code evaluates them when that code
	// uses import() or import.meta or holds a direct eval, and with a nil
	// referrer otherwise, as when a job calls eval or a constructor
	// directly, with no code of a script or module running
	// (Promise.resolve(s).then(eval)). It runs within import(), and its
	// error rejects import()'s promise: an *Exception with its value, a
	// *SyntaxError with a SyntaxError, an *InterruptedError stops the
	// running code, any other error rejects with an Error. The modules a
	// returned module imports are resolved through Resolve too, as Link
	// does.
	Resolve Resolver
	// Meta, when set, fills the import.meta object of module m when a
	// runtime first evaluates import.meta in m's code. The object has a null
	// prototype and no properties. Its error is thrown by the import.meta
	// expression, and the next evaluation calls Meta again.
	Meta func(r *Realm, m *Module, meta *Object) error
	// contains filtered or unexported fields
}

Importer is the host's side of import() and import.meta in the runtimes whose Options name it. One Importer may serve any number of runtimes, concurrently; its fields must not change once a runtime uses it, and it must not be copied then. Without an Importer, import() rejects with a TypeError and import.meta is an empty object.

import(specifier) asks Resolve for the module at once and links and evaluates it from a job: its promise settles with the module's namespace or what the resolution, the linking or the evaluation failed with. The module is the identity, as for Link: a runtime evaluates one module once, whether its code imports it statically or dynamically, so a module the runtime evaluated before is not evaluated again. The graph of a module is linked once per Importer (the graph of a module Link returned is its own), so each runtime only instantiates and evaluates it.

A module whose evaluation an interrupt stopped stays failed in the runtime: one whose top level the interrupt stopped, before or after an await; one whose next step (resuming its top level, or running it once the modules it imports evaluated) the interrupt dropped with the runtime's queued jobs; and one that imports such a module. After ClearInterrupt, an import() whose graph reaches it rejects with an Error whose message is `Cannot import "<specifier>": its evaluation was interrupted`, with the specifier import() was given. The Error holds neither the interrupt's value nor a Go error: thrown out of a hook, its *Exception unwraps to nil, where the Error of a Resolve error unwraps to that error. An import() in flight when the interrupt came stays pending, and so does a module whose top level awaits a promise the interrupt left pending, as other code awaiting it does.

Resolve and Meta are called from every goroutine that uses the Importer, concurrently. Each call runs within the call of the runtime that evaluates the import() or import.meta, or runs the job that links the module import() loaded, and Resolve may call back into that runtime.

What the Importer and its runtimes keep grows with the distinct code they see. The Importer holds every module with imports that import() loaded, with its graph, for as long as it lives. A runtime holds a record of every distinct script and module whose code uses import() or import.meta or holds a direct eval and that it ran, with the *Script or *Module, for as long as it lives, however many times the code runs, and an entry for each piece of code it compiled from a string in them (eval code and the code of the Function constructors). A host that generates modules or scripts at run time must bound how many one Importer loads and one runtime runs.

type InternalError

type InternalError struct {
	Value any    // the recovered panic value
	Stack []byte // the goroutine stack at the panic
}

InternalError is a Go panic that escaped the engine: an engine bug or a host function that panicked. The runtime's call state is restored and the jobs left queued are dropped, but the host should drop the runtime.

func (*InternalError) Error

func (e *InternalError) Error() string

func (*InternalError) Unwrap

func (e *InternalError) Unwrap() error

Unwrap returns the panic value when it is an error.

type InterruptedError

type InterruptedError = engine.InterruptedError

InterruptedError is returned when Interrupt stopped running code.

type MemoryLimitError

type MemoryLimitError = engine.MemoryLimitError

MemoryLimitError is the value of the *InterruptedError a runtime stops with when it allocates more than Options.MemoryLimit between resets; it unwraps to ErrMemoryLimit.

type Module

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

Module is a compiled ES module. It is immutable: any number of runtimes may load it, concurrently.

func Compile

func Compile(name, source string) (*Module, error)

Compile parses and compiles an ES module. Bad source, including features the engine does not support yet, is a *SyntaxError. A module that imports others is loaded once Link linked it with them.

func CompileTS

func CompileTS(name, source string) (*Module, error)

CompileTS is Compile for TypeScript source. The parser erases the type syntax as it reads it, so a *SyntaxError and a stack trace refer to the TypeScript text, and Function.prototype.toString returns it. An import specifier whose binding is only used as a type is dropped, and so is an import declaration left without bindings, as tsc drops them; an export of a type is dropped too. TypeScript that has run-time semantics (enum, a namespace with values, parameter properties, import = require, export =) is a *SyntaxError. The module links and loads like any other; a Resolver or an Importer may return modules compiled either way.

func Link(entry *Module, resolve Resolver) (*Module, error)

Link resolves the modules entry imports, directly or not, asking resolve once per module and specifier, links them and returns entry linked with its graph: the Module runtimes load, whose Hook and Exports also reach the names entry re-exports. Linking happens once; the Module is immutable, and each runtime that loads it only instantiates and evaluates the graph. The Hooks of entry's own exports stay valid for the linked Module. An import that does not resolve to one binding is a *SyntaxError at the import in the importing module, and so is an indirect export; a failed resolution is a *ResolveError. Link runs no JavaScript: a panic of resolve propagates. A module that imports nothing and uses neither import() nor import.meta is returned as is.

func (*Module) Exports

func (m *Module) Exports() []string

Exports returns the module's export names, sorted: for a module Link returned, the names it re-exports too, without those its star exports leave ambiguous.

func (*Module) Hook

func (m *Module) Hook(export string, members ...string) (Hook, error)

Hook names an exported function, or a function below an exported object reached through own properties (members "openai", "decodeRequest" of export "protocols"). The export slot and the property keys are resolved here, once; the bindings stay live, so each Call reads the export and walks the members in the runtime at hand. An export the module does not declare, or re-export once linked, is ErrHookNotFound.

func (*Module) Name

func (m *Module) Name() string

Name returns the name given to Compile.

func (*Module) Requests

func (m *Module) Requests() []string

Requests returns the specifiers the module imports from, in the order they first appear in its source, nil when it imports none.

type NativeFunc

type NativeFunc = engine.NativeFunc

NativeFunc is a host function. Returning an error throws: an *Exception or *InterruptedError as is, any other error as an Error whose message is err.Error() and which unwraps to err.

type Object

type Object = engine.Object

Object is a JavaScript object.

type Options

type Options struct {
	// MutableIntrinsics gives the runtime its own mutable copy of the
	// builtins (prototypes, constructors, Math, JSON). By default they are
	// built once per process, deeply frozen and shared by every runtime, and
	// writing to them throws a TypeError.
	MutableIntrinsics bool
	// TimeZone is the local time zone of Date; nil means time.Local.
	TimeZone *time.Location
	// Importer resolves the modules of import() and fills import.meta; nil
	// means import() rejects with a TypeError.
	Importer *Importer
	// MaxDynamicSource is the length in UTF-8 bytes of the longest source
	// text eval, the Function constructors and Realm.EvalScript compile:
	// longer text throws a RangeError. Zero
	// means engine.DefaultMaxDynamicSource (1 MiB), a negative value no
	// limit (engine.RealmOptions). Compile and CompileScript have no limit.
	MaxDynamicSource int
	// DisableDynamicCode makes eval of a string, the Function constructors
	// and Realm.EvalScript throw an EvalError instead of compiling anything,
	// for hosts whose code needs none (engine.RealmOptions). Compile and
	// CompileScript are not affected.
	DisableDynamicCode bool
	// MemoryLimit is the number of bytes the runtime's JavaScript may
	// allocate between resets (ReleaseCallData, ResetAllocation, and the
	// end of a Load or SetGlobal that succeeded) before the runtime
	// interrupts itself: the call returns an *InterruptedError whose value
	// is a *MemoryLimitError, which errors.Is finds as ErrMemoryLimit. Zero
	// means no limit. The guide's "Memory limit" tells what is counted.
	MemoryLimit int64
}

Options configures NewRuntime.

type PromiseRejectionOperation

type PromiseRejectionOperation = engine.PromiseRejectionOperation

PromiseRejectionOperation is what a rejection tracker is told (see SetPromiseRejectionTracker).

type PromiseState

type PromiseState = engine.PromiseState

PromiseState is the state of a promise.

type Realm

type Realm = engine.Realm

Realm is the engine state a Runtime owns; host functions receive it.

type Referrer

type Referrer interface {
	// Name returns the name the code was compiled with.
	Name() string
	// contains filtered or unexported methods
}

A Referrer is the code that requests a module: a *Module or a *Script.

type ResolveError

type ResolveError struct {
	File      string
	Line      int // 1-based
	Column    int // 1-based, in code points
	Specifier string
	Err       error
}

ResolveError is Link's error when the resolver fails for an import, an export-from or a star export of a module, at File, Line and Column of that entry: Err is the resolver's error, ErrNoResolver, or an error when the resolver returned no module. Load returns one too when the graph resolves the request of a module the runtime instantiated before (from import()) to another module than the runtime did; an import() of such a graph rejects with an Error of it.

func (*ResolveError) Error

func (e *ResolveError) Error() string

Error formats as `file:line:col: cannot resolve module "specifier": err`.

func (*ResolveError) Unwrap

func (e *ResolveError) Unwrap() error

type Resolver

type Resolver func(referrer Referrer, specifier string) (*Module, error)

Resolver returns the module that specifier names in referrer (HostLoadImportedModule). Link asks it for the requests of the modules of a graph: the referrer is the importing *Module, Link's entry or a module the resolver returned. An Importer asks it for import() too, where the referrer can also be a *Script, or nil. moejs never reads files or the network: the host maps every specifier to a module it compiled. The module is the identity: a runtime evaluates one module once, so a resolver returns the same *Module for the same module every time, which also compiles a module shared by several graphs once. A module Link returned stands for the module it linked: its graph is not used, and Link resolves its requests again.

type Runtime

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

Runtime is one JavaScript global environment with at most one loaded module. It must be used from one goroutine at a time; only Interrupt and ClearInterrupt may be called concurrently.

The objects its JavaScript creates are its own. Any object of another runtime can run that runtime's code when it is used, not only a function or generator but also through a method, an accessor, a proxy, a promise or a thenable, and that code reads the caches of the runtime running it: wrong results or an *InternalError. Only Go values (ToGo, FromGo) and JSON are safe to pass between runtimes. Call, CallFunction, SetGlobal, FromGo and the settlers of NewPromise return ErrForeign for a function or generator of another runtime (or a bound function or proxy of one), FromGo also for one inside the Go containers it converts (reading that member throws); they do not look inside other objects, and do not check a proxy of another runtime that is not callable, even inside a Go map: its traps run in the runtime that reads it.

func NewRuntime

func NewRuntime(opts Options) *Runtime

NewRuntime creates a runtime.

func (*Runtime) AppendJSON

func (rt *Runtime) AppendJSON(dst []byte, v Value) (out []byte, err error)

AppendJSON appends JSON.stringify(v) to dst as UTF-8. A value with no JSON form (undefined, a function, a symbol) appends null. A proxy is serialized as JSON.stringify does it: its traps run. Nesting deeper than 10,000 arrays and objects (engine.MaxToGoDepth) is a RangeError, and so is an output of more than 2^30-24 bytes: the string length limit counted in bytes of UTF-8, JSON.stringify's limit for ASCII text and reached first for other text.

The output is written into dst's spare capacity, after reserving the last output's length plus an eighth: a host that passes its previous output back (buf, err = rt.AppendJSON(buf[:0], v)) allocates only when buf has less room than that, and after one very large output a call with a small dst allocates that much again. Before any JavaScript runs (a toJSON, a Date's included, a getter, a proxy's trap) the output moves to a buffer of its own, which holds twice what is written so far and grows as the rest is written, and is appended to dst when AppendJSON returns, so a host function that JavaScript calls meanwhile may append to dst or pass it to another AppendJSON; what it appended to dst's spare capacity is overwritten then, as append(dst, text...) would overwrite it. The same holds for a job (a promise reaction, queueMicrotask) that code queued, when AppendJSON is called outside any Call: the job runs before the output is appended to dst. Inside a host function the jobs run when the outermost Call returns, after AppendJSON has returned, and one that appends to the same dst overwrites what AppendJSON appended, as any later append would: a host function should use a buffer of its own.

func (*Runtime) Call

func (rt *Runtime) Call(h Hook, args ...Value) (res Value, err error)

Call invokes the function h names with this = undefined. A path that does not lead to a value is ErrHookNotFound, one that leads to a value that is not a function ErrNotCallable; a throw is an *Exception, an interrupt an *InterruptedError, a Go panic below it an *InternalError. The hooks of a module whose top level failed return Load's error. An argument that is a function or generator of another runtime is ErrForeign. A Call made inside a host function gives its *InternalError to that host function, as CallFunction does: returned to JavaScript, it is an Error JavaScript can catch.

func (*Runtime) CallFunction

func (rt *Runtime) CallFunction(fn, this Value, args ...Value) (res Value, err error)

CallFunction calls the function fn with this and args, for a host that calls a function JavaScript gave it, such as a callback passed to a host function, after that call returned. It works as Call does: a value that is not a function is ErrNotCallable; a throw is an *Exception, an interrupt an *InterruptedError, also one that arrived before a host function fn runs, and a Go panic below it an *InternalError; fn, this or an argument that is a function or generator of another runtime is ErrForeign. The jobs the call queues run before it returns, and the first exception they throw is its error when it succeeded otherwise. Called inside another call (from a host function), it leaves them to the end of the outermost one, and its *InternalError goes to that host function: returned to JavaScript like any other Go error, it is thrown as an Error that JavaScript can catch, and reaches the outermost call, uncaught, as an *Exception that unwraps to it.

func (*Runtime) ClearInterrupt

func (rt *Runtime) ClearInterrupt()

ClearInterrupt drops a pending interrupt.

func (*Runtime) Export

func (rt *Runtime) Export(name string) (v Value, ok bool)

Export returns the current value of the loaded module's export name, a name it re-exports included. ok is false when there is no such export or the binding is not initialized yet, and, once the module's top level failed (Load), for a function or a module namespace object, whose code could find the module's other bindings uninitialized without the ReferenceError.

func (*Runtime) FromGo

func (rt *Runtime) FromGo(v any) (Value, error)

FromGo converts a Go value: nil, bool, the integer and float kinds, string, json.Number, *big.Int (a bigint), Value, NativeFunc, and the JSON-shaped containers map[string]any, map[string]string, map[string][]string, []any, []string and []map[string]any. Containers convert lazily, one level when first touched, so a large argument the hook reads little of costs little; the Go value must not change while the result is in use, and JavaScript writes never reach it. Maps enumerate their keys sorted. A []byte becomes an ArrayBuffer over the same bytes, not a copy: JavaScript writes reach them, and the host must not modify them while JavaScript may read them.

A json.RawMessage converts as JSON.parse of a copy of its text, an object or array text parsed when JavaScript first reads it; a named type as its underlying type; any other type encoding/json writes as JSON.parse of its json.Marshal text, a snapshot. An error without a MarshalJSON or MarshalText method, a struct none of whose fields json.Marshal writes, a channel, a func that is not a NativeFunc and a complex number are an error; inside a container, reading that member throws a TypeError. The guide's "Go values in" and "JSON in" give the details. A Value or *Object that is a function or generator of another runtime is ErrForeign; inside a container, reading its member throws a TypeError with ErrForeign's text.

Example (RawMessage)

ExampleRuntime_FromGo_rawMessage passes stored JSON text: the hook reads it as an object, and the engine parses it only when the hook reads it.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/Calcium-Ion/moejs"
)

func main() {
	mod, _ := moejs.Compile("hook.js", `export function step(ctx) { return ctx.state.step + 1; }`)
	hook, _ := mod.Hook("step")
	rt := moejs.NewRuntime(moejs.Options{})
	_ = rt.Load(mod)

	stored := json.RawMessage(`{"step": 2, "history": [1, 2]}`)
	arg, _ := rt.FromGo(map[string]any{"state": stored})
	res, _ := rt.Call(hook, arg)
	fmt.Println(res.String())
	rt.ReleaseCallData()
}
Output:
3
Example (Struct)

ExampleRuntime_FromGo_struct passes a struct and a named map: the struct arrives as JSON.parse of its json.Marshal text, the named map as its underlying map[string]any.

package main

import (
	"fmt"

	"github.com/Calcium-Ion/moejs"
)

type userSetting map[string]any

type taskView struct {
	ID       int64             `json:"id"`
	Status   string            `json:"status"`
	Setting  userSetting       `json:"setting"`
	Headers  map[string]string `json:"headers,omitempty"`
	internal string
}

func main() {
	mod, _ := moejs.Compile("hook.js", `export function describe(task) {
		return task.status + " " + task.setting.lang + " " + Object.keys(task);
	}`)
	hook, _ := mod.Hook("describe")
	rt := moejs.NewRuntime(moejs.Options{})
	_ = rt.Load(mod)

	arg, err := rt.FromGo(taskView{ID: 3, Status: "running", Setting: userSetting{"lang": "en"}})
	if err != nil {
		panic(err)
	}
	res, _ := rt.Call(hook, arg)
	fmt.Println(res.String())
}
Output:
running en id,status,setting

func (*Runtime) Function

func (rt *Runtime) Function(name string, length int, fn NativeFunc) Value

Function creates a host function with a name and a length, the values of its `name` and `length` properties; a negative length is 0, as no function's length is negative.

func (*Runtime) Get

func (rt *Runtime) Get(v Value, key string) (res Value, err error)

Get reads property key of v, running a getter and walking the prototype chain; undefined and null have no properties and read as undefined.

func (*Runtime) Has

func (rt *Runtime) Has(h Hook) (ok bool, err error)

Has reports whether h names a function in this runtime now. A getter on the path that throws is returned as the error, and so is Load's for the hooks of a module whose top level failed.

func (*Runtime) Interrupt

func (rt *Runtime) Interrupt(v any)

Interrupt stops running code: the pending Call (or Load, ToGo, ...) returns an *InterruptedError carrying v. It may be called from any goroutine. An interrupt that arrives while nothing runs stops the next Call, whatever the function it names, or Load, and any other method once it runs JavaScript, so a host calls ClearInterrupt before reusing the runtime.

func (*Runtime) Load

func (rt *Runtime) Load(m *Module) (err error)

Load evaluates m's top level in this runtime. A runtime loads one module once, and a Load made while the top level runs is refused too. A module that imports others loads once Link linked it: Load then evaluates the modules of its graph first, each once, in the order of their imports; a failure of one of them is a failure of m's top level, which then does not run. In a runtime with an Importer, import() of m, or of a module of its graph, finds the instance Load evaluated. When the top level throws, is interrupted or panics (an *InternalError) before it ends or first awaits, the error is returned and the module stays loaded: Export reads the bindings initialized before the failure other than functions and module namespaces (none when an interrupt stopped the top level before it started), and Call and Has return the error for the module's hooks: a function could find the other bindings uninitialized. The jobs the top level queued run after it, when Export and the module's hooks find its bindings; while the top level itself runs they find none. A module with top-level await evaluates asynchronously: its top level resumes from those jobs, with the bindings found, and Load returns once none is left, with what the evaluation rejected with (an interrupt of those jobs wins), or ErrModulePending when it still awaits; an exported function called while it awaits throws a ReferenceError for a binding it has not initialized yet.

Load is setup: when it succeeds, or returns ErrModulePending, it ends by starting a new budget of Options.MemoryLimit (ResetAllocation), so the top level's allocation does not count against the first request. A failed Load leaves the count, and the error of a memory limit hit in Stats, as they are.

func (*Runtime) Module

func (rt *Runtime) Module() *Module

Module returns the loaded module, or nil.

func (*Runtime) NewPromise

func (rt *Runtime) NewPromise() (p Value, resolve, reject func(v Value) error)

NewPromise creates a pending promise, for a host function to return, and the functions that settle it. resolve does what the promise's resolve function does in JavaScript: it adopts a thenable and fulfills with any other value; reject rejects. The first call of either decides and later calls do nothing. Both may be called after the host function returned, from the goroutine that uses the runtime: called outside a Call, they run the jobs they queue before returning, as the end of a Call does, and return what a Call would for those (an *InterruptedError, or the first exception a queueMicrotask callback threw). A function or generator of another runtime is ErrForeign and settles nothing.

func (*Runtime) ParseJSON

func (rt *Runtime) ParseJSON(b []byte) (Value, error)

ParseJSON is JSON.parse of b. Nesting deeper than 10,000 arrays and objects (engine.MaxToGoDepth) is a RangeError. It parses a copy of b, so the host may reuse b once it returns; for a large text, ParseJSONString saves the copy.

func (*Runtime) ParseJSONString

func (rt *Runtime) ParseJSONString(s string) (Value, error)

ParseJSONString is ParseJSON of s without the copy when s is valid UTF-8 (other text is converted first, by both): the strings of the result, and the Go strings ToGo, Unmarshal, ToGoInto and Value.String give for them, may share s's memory, as ParseJSON's share its copy, and keep it alive.

A host that holds the text as a []byte may pass unsafe.String(unsafe.SliceData(b), len(b)) if it never modifies b while anything may still read the text: until ReleaseCallData, and for as long as the host or the module keeps values derived from it. The host is then responsible for that; nothing checks it.

Example

ExampleRuntime_ParseJSONString parses a request body the host holds as a []byte without copying it: the host does not modify the body until the request is done and nothing derived from it is kept.

package main

import (
	"fmt"
	"unsafe"

	"github.com/Calcium-Ion/moejs"
)

func main() {
	mod, _ := moejs.Compile("hook.js", `export function decode(req) { return req.model + " " + req.image.length; }`)
	hook, _ := mod.Hook("decode")
	rt := moejs.NewRuntime(moejs.Options{})
	_ = rt.Load(mod)

	body := []byte(`{"model": "gpt-image-1", "image": "iVBORw0KGgo="}`)
	arg, _ := rt.ParseJSONString(unsafe.String(unsafe.SliceData(body), len(body)))
	res, _ := rt.Call(hook, arg)
	fmt.Println(res.String())
	rt.ReleaseCallData()
}
Output:
gpt-image-1 12

func (*Runtime) Realm

func (rt *Runtime) Realm() *Realm

Realm returns the runtime's engine state, for host functions and tests that need the engine API directly.

func (*Runtime) ReleaseCallData

func (rt *Runtime) ReleaseCallData()

ReleaseCallData drops the runtime's references to the host values of finished calls, so a pooled runtime does not keep the last request alive. A host that pools runtimes calls it after the request's last use of the runtime (Call, ToGo, Get, AppendJSON: each can run JavaScript), before it returns the runtime to its pool; Call does not, so that a request running several hooks converts its data as one, and hosts that do not pool pay nothing. Values already returned stay valid: their nodes keep their own storage, so ToGo and Get of them work after it. It also starts a new budget of Options.MemoryLimit (ResetAllocation). Between calls only; a no-op inside one (from a host function).

What JavaScript keeps stays alive with what it references: a string of a FromGo argument the module stores keeps every string converted in its period (up to 512), and an object it stores keeps its chunk and, through the root placeholder in it, the whole argument; a substring of an ASCII string shares its bytes, so storing a slice of a large one keeps all of it. The names the runtime keeps for its caches (property names, RegExp patterns) are its own copies, whether they come from map keys, a parsed JSON text or a slice of a string.

func (*Runtime) ResetAllocation

func (rt *Runtime) ResetAllocation()

ResetAllocation starts a new budget of Options.MemoryLimit: what the runtime counted since the last reset goes back to zero. ReleaseCallData, Load and SetGlobal do it too; a host that does not pool runtimes calls this where a request starts. It leaves a pending interrupt pending.

func (*Runtime) RunScript

func (rt *Runtime) RunScript(s *Script) (res Value, err error)

RunScript runs s in the runtime's global environment and returns its completion value. Its var and function declarations become properties of the global object; its let, const and class declarations are global bindings that later scripts and the loaded module (before or after Load) see too. A declaration that conflicts with an existing global binding throws before anything runs. The jobs the script queued run before RunScript returns. A throw is an *Exception, an interrupt an *InterruptedError.

func (*Runtime) SetGlobal

func (rt *Runtime) SetGlobal(name string, v any) (err error)

SetGlobal assigns the global variable name. v is converted by FromGo, so a host installs a namespace of functions as a map[string]any with NativeFunc values. It writes the global object, whose property a script's global let, const or class declaration of the same name shadows (ECMA-262 9.1.1.4.1). Like Load, it is setup: called outside any call, it ends by starting a new budget of Options.MemoryLimit when it succeeds.

func (*Runtime) SetPromiseRejectionTracker

func (rt *Runtime) SetPromiseRejectionTracker(f func(p Value, op PromiseRejectionOperation))

SetPromiseRejectionTracker registers f to be called when a promise is rejected with no handler (PromiseRejectionReject) and when a promise so rejected gets its first handler (PromiseRejectionHandle), ECMAScript's HostPromiseRejectionTracker. A promise told Reject and not Handle when a Call returns has an unhandled rejection. f runs synchronously, in the reject or then that caused the operation. nil removes the tracker.

func (*Runtime) StackTrace

func (rt *Runtime) StackTrace(exc *Exception) string

StackTrace returns the `stack` of an Error thrown in this runtime: "Name: message" and one " at ..." line per frame. It is "" when the thrown value is not an Error or its stack was replaced by a non-string. No user code runs.

func (*Runtime) Stats

func (rt *Runtime) Stats() Stats

Stats returns the runtime's counters, each read in constant time; the allocation counters count only while a MemoryLimit is set. It runs on the goroutine using the runtime, also from a host function.

Example

ExampleRuntime_Stats exposes the runtime's counters to JavaScript through a host function and reports a request that passed the memory limit.

package main

import (
	"errors"
	"fmt"

	"github.com/Calcium-Ion/moejs"
)

func main() {
	mod, _ := moejs.Compile("plugin.js", `
export function handle() {
	const before = memoryUsage();
	const rows = [];
	for (let i = 0; i < 100; i++) rows.push({i});
	return memoryUsage() > before;
}
export function runaway() { const a = []; for (;;) a.push([a.length]); }`)
	rt := moejs.NewRuntime(moejs.Options{MemoryLimit: 8 << 20})
	_ = rt.SetGlobal("memoryUsage", moejs.NativeFunc(func(r *moejs.Realm, this moejs.Value, args []moejs.Value) (moejs.Value, error) {
		return moejs.Int(r.Stats().RequestAllocatedBytes), nil
	}))
	_ = rt.Load(mod) // setup: Load ends with a new budget

	handle, _ := mod.Hook("handle")
	v, _ := rt.Call(handle)
	fmt.Println(v.String())

	runaway, _ := mod.Hook("runaway")
	_, err := rt.Call(runaway)
	var mle *moejs.MemoryLimitError
	fmt.Println(errors.Is(err, moejs.ErrMemoryLimit), errors.As(err, &mle) && mle.Limit == 8<<20)
	// The request is over: drop the interrupt and start a new budget.
	rt.ClearInterrupt()
	rt.ReleaseCallData()
	v, _ = rt.Call(handle)
	fmt.Println(v.String(), rt.Stats().MemoryLimitHits)
}
Output:
true
true true
true 1

func (*Runtime) ThrownValue

func (rt *Runtime) ThrownValue(err error) (v Value, ok bool)

ThrownValue returns the value JavaScript catches when a host function returns err: an *Exception's value, and for any other error an Error whose message is err.Error() and which, once thrown back to the host as an *Exception, unwraps to err. A host rejects a promise of NewPromise with it to give JavaScript the error a host function would have thrown. ok is false for nil and for an *InterruptedError, which JavaScript cannot catch. An *Exception must come from this runtime: one of another runtime gives that runtime's value, which this one must not use (Runtime). No JavaScript runs.

func (*Runtime) ToGo

func (rt *Runtime) ToGo(v Value) (out any, err error)

ToGo exports v: undefined and null become nil, booleans bool, strings string, integral numbers in int64 range int64 (except -0), other numbers float64, arrays []any, other objects map[string]any of their own enumerable string-keyed properties, Date time.Time, bigint *big.Int, and functions and symbols the engine value itself (*Object, *engine.Symbol). An ArrayBuffer or SharedArrayBuffer exports a copy of its bytes as a []byte, and a typed array or DataView a copy of the bytes it views (so a Uint16Array of 2 elements gives 4 bytes, little-endian); a detached buffer and a view out of its buffer's bounds give a nil []byte, and a zero-length buffer or view a non-nil empty one. A proxy exports through its traps (see engine.Realm.ToGo): []any when its target is an array, the map of its enumerable keys otherwise, the *Object when it is callable. An array or object FromGo converted from a non-empty Go map or slice exports as that Go value while JavaScript has not modified it (engine.Object.HostValue); an empty one converted to a plain array or object, and a nil map to null. A getter or trap that throws, a revoked proxy, or nesting deeper than 10,000 containers (engine.MaxToGoDepth) is returned as the error. The Go string of an ASCII string is the one v holds: for a value ParseJSON or ParseJSONString produced it may share the parsed text and keep it alive; strings.Clone what is kept.

func (*Runtime) ToGoInto

func (rt *Runtime) ToGoInto(v Value, target any) error

ToGoInto stores v in the value target points to as json.Unmarshal stores the text json.Marshal writes for what ToGo returns for v, and returns the error ToGo, json.Marshal or json.Unmarshal would. A value made of ordinary objects (class instances and objects with a null prototype included), arrays, strings, numbers, booleans, null and undefined, and arguments FromGo converted that it returns untouched (in an `any` of target), goes into target without the round trip: no Go map or slice of ToGo is built, no JSON is written or parsed, and strings are not copied. A json.RawMessage in target receives json.Marshal's text of ToGo's value for its part of v (keys sorted, <, > and & escaped), the bytes the round trip gives it, without the round trip for the rest; an untouched RawMessage argument passes through as json.Marshal compacts it. Anything else (a getter, a proxy, a Date, a Map, a typed array, a BigInt, a function, a target type that unmarshals itself, a value target's type rejects) makes it fall back to ToGo, json.Marshal and json.Unmarshal, with their result and error. When ToGo or json.Marshal fails (a getter that throws, a NaN or an infinity, a cycle), target is left as it was. After an error of json.Unmarshal, target may hold part of v, as with json.Unmarshal.

The result differs from Unmarshal's where ToGo's Go value differs from AppendJSON's text: a member whose value is undefined is kept, as null; -0 stays -0; a NaN or an infinity is json.Marshal's error; toJSON is not called; an argument FromGo converted that JavaScript has not modified is its Go value as json.Marshal writes it (a nil slice as null). No interrupt is observed, as ToGo observes none for such a value. A string of a value ParseJSON or ParseJSONString produced may share the parsed text and keep it alive for as long as the host holds it: strings.Clone a string kept beyond the request.

Example (RawMessage)

ExampleRuntime_ToGoInto_rawMessage keeps a subtree of a hook's result as JSON text: the json.RawMessage field receives what json.Marshal writes for it, and the rest of the struct is filled without a round trip.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/Calcium-Ion/moejs"
)

func main() {
	mod, _ := moejs.Compile("hook.js", `export function submit() {
		return { taskId: "t1", state: { attempt: 1, ids: ["b", "a"] } };
	}`)
	hook, _ := mod.Hook("submit")
	rt := moejs.NewRuntime(moejs.Options{})
	_ = rt.Load(mod)

	res, _ := rt.Call(hook)
	var out struct {
		TaskID string          `json:"taskId"`
		State  json.RawMessage `json:"state"`
	}
	if err := rt.ToGoInto(res, &out); err != nil {
		panic(err)
	}
	fmt.Println(out.TaskID, string(out.State))
}
Output:
t1 {"attempt":1,"ids":["b","a"]}

func (*Runtime) ToGoIntoWith

func (rt *Runtime) ToGoIntoWith(v Value, target any, opts DecodeOptions) error

ToGoIntoWith is ToGoInto with the limits of opts: when json.Marshal's text of what ToGo returns for v passes one, it returns an error wrapping ErrTooLarge, and target is left as it was. The text is counted on v before anything is decoded or allocated for it, without being written, for every value ToGoInto stores without the round trip and for the arguments FromGo converted; a value that is read by running JavaScript (a getter, a proxy) or that ToGo converts to another kind of Go value (a Date, a Map, a typed array) is counted on ToGo's result, before json.Marshal writes it. With no limit set it is ToGoInto.

Example

ExampleRuntime_ToGoIntoWith bounds a hook's result before anything is decoded: a result whose JSON would pass 1 MiB is rejected with ErrTooLarge, and the target stays as it was.

package main

import (
	"encoding/json"
	"errors"
	"fmt"

	"github.com/Calcium-Ion/moejs"
)

func main() {
	mod, _ := moejs.Compile("hook.js", `export function submit() {
		return { taskId: "t1", state: { blob: "x".repeat(2 << 20) } };
	}`)
	hook, _ := mod.Hook("submit")
	rt := moejs.NewRuntime(moejs.Options{})
	_ = rt.Load(mod)

	res, _ := rt.Call(hook)
	var out struct {
		TaskID string          `json:"taskId"`
		State  json.RawMessage `json:"state"`
	}
	err := rt.ToGoIntoWith(res, &out, moejs.DecodeOptions{MaxBytes: 1 << 20})
	fmt.Println(errors.Is(err, moejs.ErrTooLarge), out.TaskID == "")
	fmt.Println(err)
}
Output:
true true
moejs: decoded value too large: more than 1048576 bytes of JSON

func (*Runtime) ToGoWith

func (rt *Runtime) ToGoWith(v Value, opts DecodeOptions) (any, error)

ToGoWith is ToGo with the limits of opts, which bound json.Marshal's text of the result: when it passes one, ToGoWith returns nil and an error wrapping ErrTooLarge. The text is counted on v before ToGo builds anything, as for ToGoIntoWith, and on ToGo's result for a value that is read by running JavaScript or that ToGo converts to another kind of Go value. A NaN or an infinity, which json.Marshal fails on, counts as null, as does a Go value json.Marshal cannot write. With no limit set it is ToGo.

func (*Runtime) Unmarshal

func (rt *Runtime) Unmarshal(v Value, target any) (err error)

Unmarshal stores v in the value target points to as json.Unmarshal stores the text AppendJSON writes for v, and returns the error json.Unmarshal (or AppendJSON) would. A value made of plain objects, arrays, strings, numbers, booleans and null, and arguments FromGo converted that it returns untouched (in an `any` of target), goes into target without the text: no JSON is written or parsed, and strings are not copied (the Go string of an ASCII string is the one v holds, as with ToGo). A json.RawMessage in target (a field, a map value, an element, target itself) receives AppendJSON's text of its value, as json.Unmarshal would give it, written into the RawMessage's own array when it fits; an untouched RawMessage argument that JavaScript returns passes through without a parse on the Go side. Anything else — a toJSON method, a getter, a proxy, a Date, a target type that unmarshals itself, a value target's type rejects — makes it fall back to AppendJSON and json.Unmarshal, with their result and error; JavaScript code then runs as AppendJSON runs it. When AppendJSON would fail (a BigInt, a cycle, a Go value it cannot write, a text past the string length limit), target is left as it was: v is checked, its Go values included, before anything is written, and a value whose text could pass the limit (counting six bytes a character of most strings) goes through the round trip. An interrupt is observed every 4096 values, as AppendJSON observes it, in the check and again in the store; an argument copied into an `any` counts a value per 4096 bytes of each of its Go strings, whether a string itself or in a map[string]string, a []string or another of its containers. One pending at the call fails a large value before anything is written, and not a small one. AppendJSON also observes one inside a long JavaScript string it escapes, which Unmarshal, copying no string, does not. After an error of json.Unmarshal (a value target's type rejects) or an interrupt, target may hold part of v, as with json.Unmarshal. A string of a value ParseJSON or ParseJSONString produced may share the parsed text and keep it alive for as long as the host holds it: strings.Clone a string kept beyond the request.

func (*Runtime) UnmarshalWith

func (rt *Runtime) UnmarshalWith(v Value, target any, opts DecodeOptions) error

UnmarshalWith is Unmarshal with the limits of opts: when AppendJSON's text of v passes one, it returns an error wrapping ErrTooLarge, and target is left as it was. The text is counted on v before anything is decoded or allocated for it, without being written, for every value Unmarshal stores without the round trip and for the arguments FromGo converted; a value that is read by running JavaScript (a toJSON, a getter, a proxy, a Date) is counted on AppendJSON's text once that is written, before json.Unmarshal reads it. With no limit set it is Unmarshal.

type Script

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

Script is a compiled classic script. It is immutable: any number of runtimes may run it, concurrently, and a runtime may run it more than once.

func CompileScript

func CompileScript(name, source string) (*Script, error)

CompileScript parses and compiles a classic script. A script is sloppy mode code unless it starts with a "use strict" directive. Bad source, including features the engine does not support yet, is a *SyntaxError.

func (*Script) Name

func (s *Script) Name() string

Name returns the name given to CompileScript.

type Stats

type Stats = engine.Stats

Stats are a runtime's counters (Runtime.Stats).

type SyntaxError

type SyntaxError struct {
	File    string
	Line    int // 1-based
	Column  int // 1-based, in code points
	Message string
}

SyntaxError is a parse or early error in module source.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Error formats as "file:line:col: SyntaxError: message".

type Value

type Value = engine.Value

Value is a JavaScript value.

func String

func String(s string) Value

String converts a Go string.

Directories

Path Synopsis
Package bytecode defines the moejs instruction set and the compiled-function template shared between the compiler and the engine's interpreter.
Package bytecode defines the moejs instruction set and the compiled-function template shared between the compiler and the engine's interpreter.
Package compiler translates the annotated AST produced by package syntax into bytecode.Function templates.
Package compiler translates the annotated AST produced by package syntax into bytecode.Function templates.
Package eventloop runs a moejs Runtime on an event loop: the Node.js timers (setTimeout, setInterval, setImmediate and their clear functions) and host work whose result comes back from another goroutine.
Package eventloop runs a moejs Runtime on an event loop: the Node.js timers (setTimeout, setInterval, setImmediate and their clear functions) and host work whose result comes back from another goroutine.
internal
gen/decomp command
Command decomp generates engine/decomp_tables.go, the character decomposition data of String.prototype.localeCompare: the Decomposition_Mapping field of UnicodeData.txt (canonical and compatibility mappings, one level each, with the compatibility tag) and the non-zero Canonical_Combining_Class values.
Command decomp generates engine/decomp_tables.go, the character decomposition data of String.prototype.localeCompare: the Decomposition_Mapping field of UnicodeData.txt (canonical and compatibility mappings, one level each, with the compatibility tag) and the non-zero Canonical_Combining_Class values.
gen/ucd command
Command ucd generates internal/regexpsyntax/unicode_tables.go, the Unicode Character Database tables of the regular-expression engine: General_Category, Script and Script_Extensions values, the binary properties of ECMA-262 table 67, the properties of strings of the v flag, simple case folding and the non-u Canonicalize mapping.
Command ucd generates internal/regexpsyntax/unicode_tables.go, the Unicode Character Database tables of the regular-expression engine: General_Category, Script and Script_Extensions values, the binary properties of ECMA-262 table 67, the properties of strings of the v flag, simple case folding and the non-u Canonicalize mapping.
regexpsyntax
Package regexpsyntax parses ECMAScript regular expression patterns.
Package regexpsyntax parses ECMAScript regular expression patterns.
Package syntax implements the moejs front end: a hand-written lexer, a recursive-descent parser producing a compact AST, early errors, and a scope-resolution pass whose annotations the compiler consumes directly.
Package syntax implements the moejs front end: a hand-written lexer, a recursive-descent parser producing a compact AST, early errors, and a scope-resolution pass whose annotations the compiler consumes directly.

Jump to

Keyboard shortcuts

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