Files
poiesis/internal/runtime/runtime.go
Evan Reichard f308970531 chore(runtime): Add logrus logging framework with structured logging
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
2026-01-29 21:32:00 -05:00

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
}