Add logrus as the primary logging framework with dependency injection pattern. All errors now use WithError() for context, and structured logging uses camelCase field names. Tag runtime service with service field for better log organization. - Add logrus dependency - Update Runtime to accept logger via dependency injection - Add WithLogger() option for logger configuration - Log errors with WithError() for context - Log async function failures with service context - Document logging practices in AGENTS.md
187 lines
4.4 KiB
Go
187 lines
4.4 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/pkg/api"
|
|
"github.com/fastschema/qjs"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"reichard.io/poiesis/internal/functions"
|
|
_ "reichard.io/poiesis/internal/stdlib"
|
|
)
|
|
|
|
type Runtime struct {
|
|
ctx *qjs.Context
|
|
opts qjs.Option
|
|
funcs map[string]functions.Function
|
|
typeDecls map[string]string
|
|
logger *logrus.Entry
|
|
}
|
|
|
|
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
|
// Create Runtime
|
|
logger := logrus.New()
|
|
r := &Runtime{opts: qjs.Option{Context: ctx}, logger: logger.WithField("service", "runtime")}
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
|
|
// Create QuickJS Context
|
|
rt, err := qjs.New(r.opts)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to create QuickJS context")
|
|
return nil, err
|
|
}
|
|
r.ctx = rt.Context()
|
|
|
|
// Populate Globals
|
|
if err := r.populateGlobals(); err != nil {
|
|
logger.WithError(err).Error("Failed to populate globals")
|
|
return nil, err
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Runtime) populateGlobals() error {
|
|
// Initialize Maps
|
|
r.funcs = make(map[string]functions.Function)
|
|
r.typeDecls = make(map[string]string)
|
|
|
|
// Load Requested Functions
|
|
allFuncs := functions.GetRegisteredFunctions()
|
|
for name, fn := range allFuncs {
|
|
r.funcs[name] = fn
|
|
if err := r.addFunctionTypes(fn); err != nil {
|
|
return fmt.Errorf("failed to add types for function %s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
// Register Functions with QuickJS
|
|
for name, fn := range r.funcs {
|
|
if fn.IsAsync() {
|
|
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
|
|
qjsVal, err := callFunc(this, fn)
|
|
if err != nil {
|
|
r.logger.WithError(err).Errorf("Async function %s failed", name)
|
|
_ = this.Promise().Reject(this.Context().NewError(err))
|
|
return
|
|
}
|
|
_ = this.Promise().Resolve(qjsVal)
|
|
})
|
|
} else {
|
|
r.ctx.SetFunc(name, func(this *qjs.This) (*qjs.Value, error) {
|
|
return callFunc(this, fn)
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addFunctionTypes adds types from a function to the runtime's type declarations.
|
|
// Returns an error if there's a type conflict (same name, different definition).
|
|
func (r *Runtime) addFunctionTypes(fn functions.Function) error {
|
|
for name, def := range fn.Types() {
|
|
if existing, ok := r.typeDecls[name]; ok && existing != def {
|
|
return fmt.Errorf("type conflict: %s has conflicting definitions (existing: %s, new: %s)",
|
|
name, existing, def)
|
|
}
|
|
r.typeDecls[name] = def
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTypeDeclarations returns all TypeScript type declarations for this runtime.
|
|
// Includes both type definitions and function declarations.
|
|
func (r *Runtime) GetTypeDeclarations() string {
|
|
var decls []string
|
|
|
|
// Add Type Definitions
|
|
for _, def := range r.typeDecls {
|
|
decls = append(decls, def)
|
|
}
|
|
|
|
// Add Function Declarations
|
|
for _, fn := range r.funcs {
|
|
decls = append(decls, fn.Definition())
|
|
}
|
|
|
|
return strings.Join(decls, "\n\n")
|
|
}
|
|
|
|
func (r *Runtime) RunFile(filePath string) error {
|
|
tsFileContent, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading file: %w", err)
|
|
}
|
|
|
|
return r.RunCode(string(tsFileContent))
|
|
}
|
|
|
|
func (r *Runtime) RunCode(tsCode string) error {
|
|
transformedCode, err := r.transformCode(tsCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := r.ctx.Eval("code.ts", qjs.Code(string(transformedCode)))
|
|
if result != nil {
|
|
result.Free()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *Runtime) transformCode(tsCode string) ([]byte, error) {
|
|
result := api.Transform(tsCode, api.TransformOptions{
|
|
Loader: api.LoaderTS,
|
|
Target: api.ES2022,
|
|
Format: api.FormatIIFE,
|
|
Sourcemap: api.SourceMapNone,
|
|
TreeShaking: api.TreeShakingFalse,
|
|
})
|
|
|
|
if len(result.Errors) > 0 {
|
|
var b strings.Builder
|
|
for i, e := range result.Errors {
|
|
if i > 0 {
|
|
b.WriteString(", ")
|
|
}
|
|
b.WriteString(e.Text)
|
|
}
|
|
return nil, fmt.Errorf("transpilation failed: %s", b.String())
|
|
}
|
|
|
|
return result.Code, nil
|
|
}
|
|
|
|
func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) {
|
|
qjsArgs := this.Args()
|
|
fnArgs := fn.Arguments()
|
|
|
|
var allArgs []any
|
|
for i := range min(len(fnArgs), len(qjsArgs)) {
|
|
rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("argument conversion failed: %w", err)
|
|
}
|
|
allArgs = append(allArgs, rVal.Interface())
|
|
}
|
|
|
|
result, err := fn.Call(this.Context(), allArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
val, err := qjs.ToJsValue(this.Context(), result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return val, nil
|
|
}
|