Add trace management to context and enhance configuration options

- Introduced a trace manager in the context struct for lazy initialization and management of trace operations.
- Implemented the Trace method in the context to facilitate trace object creation and management within JavaScript.
- Enhanced configuration options for trace drivers, including defaults for local and store drivers, and added path and prefix settings.
- Updated related tests to verify the functionality of the new trace management features.
This commit is contained in:
Max 2025-11-19 12:27:42 +08:00
parent 251b0591a2
commit c25fbb22d7
12 changed files with 1865 additions and 56 deletions

View file

@ -2,12 +2,16 @@ package context
import (
"context"
"fmt"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/trace"
traceTypes "github.com/yaoapp/yao/trace/types"
)
// New create a new context
@ -62,8 +66,19 @@ func WithTimeout(parent Context, timeout time.Duration) (Context, context.Cancel
return parent, cancel
}
// Release the context and clean up all resources including stacks
// Release the context and clean up all resources including stacks and trace
func (ctx *Context) Release() {
// Complete and release trace if exists
if ctx.trace != nil && ctx.Stack != nil && ctx.Stack.TraceID != "" {
// Mark trace as complete (sends final event)
_ = ctx.trace.MarkComplete()
// Release from global registry (removes from registry and closes resources)
_ = trace.Release(ctx.Stack.TraceID)
ctx.trace = nil
}
// Clear space
if ctx.Space != nil {
ctx.Space.Clear()
@ -98,6 +113,70 @@ func (ctx *Context) Send(data []byte) error {
return err
}
// Trace returns the trace manager for this context, lazily initialized on first call
// Uses the TraceID from ctx.Stack if available, or generates a new one
func (ctx *Context) Trace() (traceTypes.Manager, error) {
// Return trace if already initialized
if ctx.trace != nil {
return ctx.trace, nil
}
// Get TraceID from Stack or generate new one
var traceID string
if ctx.Stack != nil && ctx.Stack.TraceID != "" {
traceID = ctx.Stack.TraceID
// Try to load existing trace first
manager, err := trace.Load(traceID)
if err == nil {
// Found in registry, reuse it
ctx.trace = manager
return manager, nil
}
}
// Get trace configuration from global config
cfg := config.Conf
// Prepare driver options
var driverOptions []any
var driverType string
switch cfg.Trace.Driver {
case "store":
driverType = trace.Store
if cfg.Trace.Store == "" {
return nil, fmt.Errorf("trace store ID not configured")
}
driverOptions = []any{cfg.Trace.Store, cfg.Trace.Prefix}
case "local", "":
driverType = trace.Local
driverOptions = []any{cfg.Trace.Path}
default:
return nil, fmt.Errorf("unsupported trace driver: %s", cfg.Trace.Driver)
}
// Create trace using trace.New (handles registry)
createdTraceID, manager, err := trace.New(ctx.Context, driverType, &traceTypes.TraceOption{
ID: traceID, // Use existing ID from Stack or empty to generate new one
}, driverOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create trace: %w", err)
}
// Update Stack with the created TraceID if needed
if ctx.Stack != nil && ctx.Stack.TraceID == "" {
ctx.Stack.TraceID = createdTraceID
}
// Store for future calls
ctx.trace = manager
return manager, nil
}
// Map the context to a map
func (ctx *Context) Map() map[string]interface{} {
data := map[string]interface{}{}

View file

@ -1,16 +1,11 @@
package context
import (
"sync"
"github.com/google/uuid"
"github.com/yaoapp/gou/runtime/v8/bridge"
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
"rogchap.com/v8go"
)
var objectsMutex = sync.Mutex{}
var objects = map[string]*Context{}
// JsValue return the JavaScript value of the context
func (ctx *Context) JsValue(v8ctx *v8go.Context) (*v8go.Value, error) {
return ctx.NewObject(v8ctx)
@ -21,10 +16,16 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set the id and release function
id := ctx.objectRegister()
jsObject.Set("__id", id)
jsObject.Set("__release", ctx.objectRelease(v8ctx.Isolate(), id))
// Set internal field count to 1 to store the goValueID
// Internal fields are not accessible from JavaScript, providing better security
jsObject.SetInternalFieldCount(1)
// Register context in global bridge registry for efficient Go object retrieval
// The goValueID will be stored in internal field (index 0) after instance creation
goValueID := bridge.RegisterGoObject(ctx)
// Set release function that will be called when JavaScript object is released
jsObject.Set("__release", ctx.objectRelease(v8ctx.Isolate(), goValueID))
// Set primitive fields in template
jsObject.Set("chat_id", ctx.ChatID)
@ -42,16 +43,28 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
jsObject.Set("accept", string(ctx.Accept))
jsObject.Set("route", ctx.Route)
// Set methods
jsObject.Set("Trace", ctx.traceMethod(v8ctx.Isolate()))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
ctx.objectRelease(v8ctx.Isolate(), id)
// Clean up: release from global registry if instance creation failed
bridge.ReleaseGoObject(goValueID)
return nil, err
}
// Store the goValueID in internal field (index 0)
// This is not accessible from JavaScript, providing better security
obj, err := instance.Value.AsObject()
if err != nil {
ctx.objectRelease(v8ctx.Isolate(), id)
bridge.ReleaseGoObject(goValueID)
return nil, err
}
err = obj.SetInternalField(0, goValueID)
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
@ -98,19 +111,49 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
return instance.Value, nil
}
func (ctx *Context) objectRegister() string {
id := uuid.NewString()
objectsMutex.Lock()
defer objectsMutex.Unlock()
objects[id] = ctx
return id
}
func (ctx *Context) objectRelease(iso *v8go.Isolate, id string) *v8go.FunctionTemplate {
// objectRelease releases the Go object from the global bridge registry
// It retrieves the goValueID from internal field (index 0) and releases the Go object
func (ctx *Context) objectRelease(iso *v8go.Isolate, goValueID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
objectsMutex.Lock()
defer objectsMutex.Unlock()
delete(objects, id)
// Get the context object (this)
thisObj, err := info.This().AsObject()
if err == nil && thisObj.InternalFieldCount() > 0 {
// Get goValueID from internal field (index 0)
goValueID := thisObj.GetInternalField(0)
if goValueID != nil && goValueID.IsString() {
// Release from global bridge registry
bridge.ReleaseGoObject(goValueID.String())
}
}
return v8go.Undefined(info.Context().Isolate())
})
}
func (ctx *Context) traceMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
// Get trace manager (lazy initialization)
manager, err := ctx.Trace()
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
// Get trace ID
traceID := ""
if ctx.Stack != nil {
traceID = ctx.Stack.TraceID
}
// Create JavaScript Trace object directly
// The Trace object will be used within JavaScript and its __release will be called
// when the JavaScript value is released via defer bridge.FreeJsValue(jsRes)
traceObj, err := traceJsapi.NewTraceObject(v8ctx, traceID, manager)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return traceObj
})
}

View file

@ -1,6 +1,7 @@
package context
import (
"context"
"fmt"
"sync"
"testing"
@ -35,7 +36,7 @@ func TestJsValue(t *testing.T) {
t.Fatalf("Call failed: %v", err)
}
assert.Equal(t, "ChatID-123456", res)
assert.Equal(t, 0, len(objects))
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testContextJsvalueEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
@ -132,7 +133,7 @@ func TestJsValueConcurrent(t *testing.T) {
// Verify all objects are cleaned up after GC
// Note: objects should be released when v8 values are garbage collected
assert.Equal(t, 0, len(objects), "All objects should be cleaned up")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
// TestJsValueRegistrationAndCleanup test the object registration and cleanup mechanism
@ -141,11 +142,6 @@ func TestJsValueRegistrationAndCleanup(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Clear objects map before test
objectsMutex.Lock()
objects = map[string]*Context{}
objectsMutex.Unlock()
v8.RegisterFunction("testContextRegistration", testContextRegistrationEmbed)
// Create multiple contexts and verify registration
@ -168,7 +164,7 @@ func TestJsValueRegistrationAndCleanup(t *testing.T) {
}
// All objects should be cleaned up after v8.Call completes
assert.Equal(t, 0, len(objects), "All objects should be cleaned up after execution")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testContextRegistrationEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
@ -186,16 +182,6 @@ func testContextRegistrationFunction(info *v8go.FunctionCallbackInfo) *v8go.Valu
return bridge.JsException(info.Context(), err)
}
// Verify the object has __id field
id, err := ctx.Get("__id")
if err != nil {
return bridge.JsException(info.Context(), err)
}
if !id.IsString() {
return bridge.JsException(info.Context(), fmt.Errorf("__id should be a string"))
}
// Verify the object has __release function
release, err := ctx.Get("__release")
if err != nil {
@ -206,14 +192,14 @@ func testContextRegistrationFunction(info *v8go.FunctionCallbackInfo) *v8go.Valu
return bridge.JsException(info.Context(), fmt.Errorf("__release should be a function"))
}
// Verify the object is registered
objectsMutex.Lock()
idStr := id.String()
_, exists := objects[idStr]
objectsMutex.Unlock()
// Verify the object has internal field (goValueID is stored in internal field, not accessible from JS)
if ctx.InternalFieldCount() == 0 {
return bridge.JsException(info.Context(), fmt.Errorf("object should have internal field"))
}
if !exists {
return bridge.JsException(info.Context(), fmt.Errorf("object %s not registered", idStr))
goValueID := ctx.GetInternalField(0)
if goValueID == nil || !goValueID.IsString() {
return bridge.JsException(info.Context(), fmt.Errorf("internal field should contain goValueID string"))
}
val, err := v8go.NewValue(info.Context().Isolate(), true)
@ -348,7 +334,7 @@ func TestJsValueAllFields(t *testing.T) {
_, hasSilent := result["silent"]
assert.False(t, hasSilent, "silent (deprecated) should not be exported")
assert.Equal(t, 0, len(objects))
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testAllFieldsEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
@ -442,3 +428,68 @@ func testAllFieldsFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
}
return jsVal
}
// TestJsValueTrace test the Trace method on Context
func TestJsValueTrace(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Stack: &Stack{
TraceID: "test-trace-id",
},
Context: context.Background(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(cxt) {
// Get trace from context
const trace = cxt.Trace()
// Verify trace object exists
if (!trace) {
throw new Error("Trace() returned null or undefined")
}
// Verify trace has expected methods
if (typeof trace.Add !== 'function') {
throw new Error("trace.Add is not a function")
}
if (typeof trace.Info !== 'function') {
throw new Error("trace.Info is not a function")
}
// Actually use the trace - add a node
const node = trace.Add({ type: "test", content: "Test from context" }, { label: "Test Node" })
// Log some info
trace.Info("Testing trace from context")
node.Info("Node info message")
// Complete the node
node.Complete({ result: "success" })
// Return verification info
return {
trace_id: trace.id,
node_id: node.id,
success: true
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
// Verify trace was accessible and operations succeeded
assert.Equal(t, "test-trace-id", result["trace_id"], "trace_id should match")
assert.NotEmpty(t, result["node_id"], "node_id should not be empty")
assert.Equal(t, true, result["success"], "operation should succeed")
}

View file

@ -6,6 +6,7 @@ import (
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/gou/store"
"github.com/yaoapp/yao/openapi/oauth/types"
traceTypes "github.com/yaoapp/yao/trace/types"
)
// Accept the accept of the request, it will be used to identify the accept of the request.
@ -120,11 +121,12 @@ type Context struct {
// Context
context.Context
Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call
Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache"
Stack *Stack `json:"-"` // Stack, current active stack of the request
Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging)
Writer Writer `json:"-"` // Writer, it will be used to write response data to the client
Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call
Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache"
Stack *Stack `json:"-"` // Stack, current active stack of the request
Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging)
Writer Writer `json:"-"` // Writer, it will be used to write response data to the client
trace traceTypes.Manager `json:"-"` // Trace manager, lazy initialized on first access
// Authorized information
Authorized *types.AuthorizedInfo `json:"authorized,omitempty"` // Authorized information

View file

@ -102,9 +102,50 @@ func Load() Config {
}
}
// Trace Driver - default based on mode
if cfg.Trace.Driver == "" {
if cfg.Mode == "development" {
cfg.Trace.Driver = "local"
} else {
cfg.Trace.Driver = "store"
}
}
// Trace Path - default to same directory as log file when using local driver
if cfg.Trace.Driver == "local" {
if cfg.Trace.Path == "" {
// Use the log file directory
logDir := cfg.GetLogDir()
cfg.Trace.Path = filepath.Join(logDir, "traces")
}
if !filepath.IsAbs(cfg.Trace.Path) {
cfg.Trace.Path = filepath.Join(cfg.Root, cfg.Trace.Path)
}
}
// Trace Prefix - default prefix for store driver
if cfg.Trace.Driver == "store" && cfg.Trace.Prefix == "" {
cfg.Trace.Prefix = "trace:"
}
return cfg
}
// GetLogDir returns the directory of the log file
func (cfg *Config) GetLogDir() string {
logPath := cfg.Log
if logPath == "" {
logPath = filepath.Join(cfg.Root, "logs", "application.log")
}
if !filepath.IsAbs(logPath) {
logPath = filepath.Join(cfg.Root, logPath)
}
return filepath.Dir(logPath)
}
// Production 设定为生产环境
func Production() {
os.Setenv("YAO_MODE", "production")

View file

@ -25,6 +25,7 @@ type Config struct {
Session Session `json:"session,omitempty"` // Session Config
Studio Studio `json:"studio,omitempty"` // Studio config
Runtime Runtime `json:"runtime,omitempty"` // Runtime config
Trace Trace `json:"trace,omitempty"` // Trace config
}
// Studio the studio config
@ -67,3 +68,11 @@ type Runtime struct {
Precompile bool `json:"precompile,omitempty" env:"YAO_RUNTIME_PRECOMPILE" envDefault:"false"` // if true compile scripts when the VM is created. this will increase the load time, but the script will run faster. the default value is false
Import bool `json:"import,omitempty" env:"YAO_RUNTIME_IMPORT" envDefault:"true"` // If false the import statement will be disabled, the default value is true.
}
// Trace config
type Trace struct {
Driver string `json:"driver,omitempty" env:"YAO_TRACE_DRIVER"` // The trace driver. local (development) | store (production)
Path string `json:"path,omitempty" env:"YAO_TRACE_PATH"` // The local file path for trace storage
Store string `json:"store,omitempty" env:"YAO_TRACE_STORE"` // The store ID when driver is "store"
Prefix string `json:"prefix,omitempty" env:"YAO_TRACE_PREFIX"` // The prefix for trace storage keys
}

View file

@ -9,6 +9,7 @@ import (
_ "github.com/yaoapp/yao/helper"
_ "github.com/yaoapp/yao/openai"
_ "github.com/yaoapp/yao/seed"
_ "github.com/yaoapp/yao/trace/jsapi"
_ "github.com/yaoapp/yao/wework"
"github.com/yaoapp/yao/cmd"

83
trace/jsapi/jsapi.go Normal file
View file

@ -0,0 +1,83 @@
package jsapi
import (
"fmt"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"rogchap.com/v8go"
)
func init() {
// Auto-register Trace JavaScript API when package is imported
v8.RegisterFunction("Trace", ExportFunction)
}
// Usage from JavaScript:
//
// const trace = new Trace({ driver: "local", path: "/tmp/traces" })
// const node = trace.Add({ type: "step", content: "Processing..." }, { label: "Step 1" })
// node.Complete({ result: "Done" })
//
// Objects:
// - Trace: Main trace manager (constructor)
// - Node: Individual trace node
// - Space: Memory space for key-value storage
// ExportFunction exports the Trace constructor function template
// This is used by v8.RegisterFunction
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, traceConstructor)
}
// traceConstructor is the JavaScript constructor for Trace
// Usage: new Trace(options)
func traceConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Parse options
options := make(map[string]interface{})
if len(args) > 0 && !args[0].IsNullOrUndefined() {
optionsJS, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid options: %s", err))
}
if optionsMap, ok := optionsJS.(map[string]interface{}); ok {
options = optionsMap
}
}
// Create trace object
traceObj, err := TraceNew(ctx, options)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return traceObj
}
// ExportLoadFunction exports the Trace.Load static method
// This can be used to load an existing trace by ID
func ExportLoadFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, traceLoadFunction)
}
// traceLoadFunction is the JavaScript function for Trace.Load
// Usage: Trace.Load(traceID)
func traceLoadFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Load requires a trace ID")
}
traceID := args[0].String()
traceObj, err := TraceLoad(ctx, traceID)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return traceObj
}

544
trace/jsapi/jsapi_test.go Normal file
View file

@ -0,0 +1,544 @@
package jsapi
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
// TestTraceNew test creating a new trace from JavaScript
func TestTraceNew(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
v8.RegisterFunction("testTraceNew", testTraceNewEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
return testTraceNew(trace)
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// After v8.Call returns, the jsRes should have been released via defer bridge.FreeJsValue(jsRes)
// This should have triggered __release() on the trace object, cleaning up the goMaps
// Note: We can't directly check goMaps as it's in the bridge package
}
func testTraceNewEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, testTraceNewFunction)
}
func testTraceNewFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) < 1 {
return bridge.JsException(info.Context(), "Missing parameters")
}
trace, err := args[0].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Verify the trace has id field
traceID, err := trace.Get("id")
if err != nil {
return bridge.JsException(info.Context(), err)
}
if !traceID.IsString() {
return bridge.JsException(info.Context(), fmt.Errorf("id should be a string"))
}
// Verify the trace has __release function
release, err := trace.Get("__release")
if err != nil {
return bridge.JsException(info.Context(), err)
}
if !release.IsFunction() {
return bridge.JsException(info.Context(), fmt.Errorf("__release should be a function"))
}
// Return the trace object itself so its __release will be called when v8.Call finishes
return args[0]
}
// TestTraceAddNode test adding nodes to trace
func TestTraceAddNode(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
v8.RegisterFunction("testTraceAddNode", testTraceAddNodeEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
// testTraceAddNode returns the trace object itself
return testTraceAddNode(trace, node)
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testTraceAddNodeEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, testTraceAddNodeFunction)
}
func testTraceAddNodeFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) < 2 {
return bridge.JsException(info.Context(), "Missing parameters")
}
trace, err := args[0].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
node, err := args[1].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Get trace ID
traceID, err := trace.Get("id")
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Get node ID
nodeID, err := node.Get("id")
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Verify IDs exist
if traceID.IsUndefined() || nodeID.IsUndefined() {
return bridge.JsException(info.Context(), fmt.Errorf("missing trace or node ID"))
}
// Return the trace object itself to trigger __release
return args[0]
}
// TestTraceNodeComplete test completing a node
func TestTraceNodeComplete(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
// Log some messages
node.Info("Processing...")
node.Debug("Debug info")
// Complete the node
node.Complete({ result: "success" })
return trace
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
// TestTraceSpace test creating and using spaces
func TestTraceSpace(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
v8.RegisterFunction("testTraceSpace", testTraceSpaceEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
const space = trace.CreateSpace({ label: "Test Space" })
// Set some values
space.Set("key1", "value1")
space.Set("key2", 123)
space.Set("key3", { nested: "object" })
// Call test function for verification
testTraceSpace(space)
// Return trace object to trigger __release
return trace
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testTraceSpaceEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, testTraceSpaceFunction)
}
func testTraceSpaceFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) < 1 {
return bridge.JsException(info.Context(), "Missing parameters")
}
space, err := args[0].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Get space ID
spaceID, err := space.Get("id")
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Get stored values to verify
getVal := func(obj *v8go.Object, method string, key string) (interface{}, error) {
getMethod, err := obj.Get(method)
if err != nil {
return nil, err
}
defer getMethod.Release()
getFn, err := getMethod.AsFunction()
if err != nil {
return nil, err
}
keyVal, err := v8go.NewValue(info.Context().Isolate(), key)
if err != nil {
return nil, err
}
defer keyVal.Release()
result, err := getFn.Call(obj.Value, keyVal)
if err != nil {
return nil, err
}
return bridge.GoValue(result, info.Context())
}
key1, _ := getVal(space, "Get", "key1")
key2, _ := getVal(space, "Get", "key2")
// Verify values exist
if spaceID.IsUndefined() || key1 == nil || key2 == nil {
return bridge.JsException(info.Context(), fmt.Errorf("missing space data"))
}
// This function is just for verification, we don't need to return a new object
// Return undefined, the outer JavaScript will return the trace object
return v8go.Undefined(info.Context().Isolate())
}
// TestTraceConcurrent test concurrent trace operations
func TestTraceConcurrent(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Number of concurrent goroutines
concurrency := 10
iterationsPerGoroutine := 5
var wg sync.WaitGroup
errors := make(chan error, concurrency*iterationsPerGoroutine)
results := make(chan string, concurrency*iterationsPerGoroutine)
// Launch concurrent goroutines
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(routineID int) {
defer wg.Done()
for j := 0; j < iterationsPerGoroutine; j++ {
script := fmt.Sprintf(`
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces-%d-%d" })
const node = trace.Add({ type: "step", content: "Test %d-%d" }, { label: "Step" })
node.Info("Processing")
node.Complete({ result: "done" })
return trace
}`, routineID, j, routineID, j)
res, err := v8.Call(v8.CallOptions{}, script)
if err != nil {
errors <- fmt.Errorf("routine %d iteration %d failed: %v", routineID, j, err)
return
}
// With Internal Field + goMaps, res should now be the manager directly
if _, ok := res.(types.Manager); ok {
// Successfully got manager, add a result
results <- fmt.Sprintf("routine-%d-iteration-%d", routineID, j)
} else {
errors <- fmt.Errorf("routine %d iteration %d: expected types.Manager, got %T", routineID, j, res)
}
}
}(i)
}
// Wait for all goroutines to complete
wg.Wait()
close(errors)
close(results)
// Check for errors
for err := range errors {
t.Error(err)
}
// Verify all results
resultCount := 0
for range results {
resultCount++
}
// Verify the correct number of results
expectedResults := concurrency * iterationsPerGoroutine
assert.Equal(t, expectedResults, resultCount, "Should have %d results", expectedResults)
// Verify all traces are cleaned up
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
// TestTraceParallel test parallel node execution
func TestTraceParallel(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
v8.RegisterFunction("testTraceParallel", testTraceParallelEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
// Must call Add() first to create root node
trace.Add({ type: "root", content: "Root" }, { label: "Root" })
// Create parallel nodes
const nodes = trace.Parallel([
{ input: { type: "task", content: "Task 1" }, option: { label: "Parallel 1" } },
{ input: { type: "task", content: "Task 2" }, option: { label: "Parallel 2" } },
{ input: { type: "task", content: "Task 3" }, option: { label: "Parallel 3" } }
])
// Call test function for verification
testTraceParallel(nodes)
// Return trace object to trigger __release
return trace
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func testTraceParallelEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, testTraceParallelFunction)
}
func testTraceParallelFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) < 1 {
return bridge.JsException(info.Context(), "Missing parameters")
}
nodes, err := args[0].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Get array length
lengthVal, err := nodes.Get("length")
if err != nil {
return bridge.JsException(info.Context(), err)
}
// Verify we got 3 nodes
if lengthVal.Int32() != 3 {
return bridge.JsException(info.Context(), fmt.Errorf("expected 3 nodes, got %d", lengthVal.Int32()))
}
// Return undefined, the outer JavaScript will return the trace object
return v8go.Undefined(info.Context().Isolate())
}
// TestTraceMarkComplete test marking trace as complete
func TestTraceMarkComplete(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
res, err := v8.Call(v8.CallOptions{}, `
function test() {
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
node.Complete({ result: "success" })
// Mark entire trace as complete
trace.MarkComplete()
// Check if complete
const isComplete = trace.IsComplete()
return trace
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With Internal Field + goMaps, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
// TestTracePassAsParameter test passing trace object as parameter to a function
func TestTracePassAsParameter(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
v8.RegisterFunction("processTrace", processTraceEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test() {
// Define a function that accepts trace as parameter
const handleTrace = function(trace) {
// Add a node using the passed trace
const node = trace.Add({ type: "step", content: "Step from handler" }, { label: "Handler Step" })
node.Info("Processing in handler function")
node.Complete({ result: "handler completed" })
// Return some info
return {
traceId: trace.id,
nodeId: node.id
}
}
// Create trace and pass it to the handler
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
const result = handleTrace(trace)
// Also test passing to a Go function
processTrace(trace)
// Return trace to trigger __release
return trace
}`)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// With __govalue function, res should now be the manager directly
manager, ok := res.(types.Manager)
if !ok {
t.Fatalf("Expected types.Manager, got %T", res)
}
assert.NotNil(t, manager, "manager should not be nil")
// Note: We can't directly check goMaps cleanup as it's in the bridge package
}
func processTraceEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, processTraceFunction)
}
func processTraceFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) < 1 {
return bridge.JsException(info.Context(), "Missing trace parameter")
}
// Try to get the trace object
traceValue := args[0]
// Convert to Go value - with __govalue function, this should return the manager directly
goValue, err := bridge.GoValue(traceValue, info.Context())
if err != nil {
return bridge.JsException(info.Context(), fmt.Errorf("failed to convert trace: %v", err))
}
fmt.Printf("\n=== Go function received trace ===\n")
fmt.Printf("Type: %T\n", goValue)
// Check if we got the manager directly (new __govalue behavior)
if manager, ok := goValue.(types.Manager); ok {
fmt.Printf("✅ SUCCESS: Got manager directly via __govalue function!\n")
fmt.Printf(" Manager type: %T\n", manager)
// Now we can use the manager directly!
manager.Info("Message from Go function via __govalue")
return v8go.Undefined(info.Context().Isolate())
}
// Fallback: if we got a map (shouldn't happen with __govalue)
if traceMap, ok := goValue.(map[string]interface{}); ok {
fmt.Printf("⚠️ Got map (fallback): %v\n", getMapKeys(traceMap))
return bridge.JsException(info.Context(), fmt.Errorf("unexpected: got map instead of manager"))
}
return bridge.JsException(info.Context(), fmt.Errorf("unexpected type: %T", goValue))
}
func getMapKeys(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}

308
trace/jsapi/node.go Normal file
View file

@ -0,0 +1,308 @@
package jsapi
import (
"fmt"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
// Helper functions
func parseTraceInput(obj *v8go.Object, ctx *v8go.Context) (types.TraceInput, error) {
goVal, err := bridge.GoValue(obj.Value, ctx)
if err != nil {
return nil, err
}
return goVal, nil
}
func parseTraceNodeOption(obj *v8go.Object) types.TraceNodeOption {
option := types.TraceNodeOption{}
if labelVal, err := obj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := obj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := obj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
return option
}
// NewNodeObject creates a JavaScript Node object (pure JS object, no Go mapping)
func NewNodeObject(v8ctx *v8go.Context, node types.Node) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set primitive fields
jsObject.Set("id", node.ID())
// Set methods
jsObject.Set("Info", nodeInfoMethod(v8ctx.Isolate(), node))
jsObject.Set("Debug", nodeDebugMethod(v8ctx.Isolate(), node))
jsObject.Set("Error", nodeErrorMethod(v8ctx.Isolate(), node))
jsObject.Set("Warn", nodeWarnMethod(v8ctx.Isolate(), node))
jsObject.Set("Add", nodeAddMethod(v8ctx.Isolate(), node))
jsObject.Set("Parallel", nodeParallelMethod(v8ctx.Isolate(), node))
jsObject.Set("SetOutput", nodeSetOutputMethod(v8ctx.Isolate(), node))
jsObject.Set("SetMetadata", nodeSetMetadataMethod(v8ctx.Isolate(), node))
jsObject.Set("Complete", nodeCompleteMethod(v8ctx.Isolate(), node))
jsObject.Set("Fail", nodeFailMethod(v8ctx.Isolate(), node))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
return nil, err
}
return instance.Value, nil
}
// Node method templates
func nodeInfoMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
node.Info(args[0].String())
}
return info.This().Value
})
}
func nodeDebugMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
node.Debug(args[0].String())
}
return info.This().Value
})
}
func nodeErrorMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
node.Error(args[0].String())
}
return info.This().Value
})
}
func nodeWarnMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
node.Warn(args[0].String())
}
return info.This().Value
})
}
func nodeAddMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "Add requires 2 arguments: (input, option)")
}
// Parse input
inputObj, err := args[0].AsObject()
if err != nil {
return bridge.JsException(ctx, "first argument must be an object")
}
input, _ := parseTraceInput(inputObj, ctx)
// Parse option
optionObj, err := args[1].AsObject()
if err != nil {
return bridge.JsException(ctx, "second argument must be an object")
}
option := parseTraceNodeOption(optionObj)
// Call node.Add
childNode, err := node.Add(input, option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create child node JS object
childNodeObj, err := NewNodeObject(ctx, childNode)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return childNodeObj
})
}
func nodeParallelMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Parallel requires an array argument")
}
// Parse array
arrayObj, err := args[0].AsObject()
if err != nil {
return bridge.JsException(ctx, "argument must be an array")
}
lengthVal, err := arrayObj.Get("length")
if err != nil {
return bridge.JsException(ctx, "invalid array")
}
length := int(lengthVal.Int32())
parallelInputs := make([]types.TraceParallelInput, 0, length)
for i := 0; i < length; i++ {
itemVal, err := arrayObj.GetIdx(uint32(i))
if err != nil {
continue
}
itemObj, err := itemVal.AsObject()
if err != nil {
continue
}
// Parse input
var input types.TraceInput
if inputVal, err := itemObj.Get("input"); err == nil && inputVal.IsObject() {
inputObjInner, _ := inputVal.AsObject()
input, _ = parseTraceInput(inputObjInner, ctx)
}
// Parse option
var option types.TraceNodeOption
if optionVal, err := itemObj.Get("option"); err == nil && optionVal.IsObject() {
optionObjInner, _ := optionVal.AsObject()
option = parseTraceNodeOption(optionObjInner)
}
parallelInputs = append(parallelInputs, types.TraceParallelInput{
Input: input,
Option: option,
})
}
// Call node.Parallel
childNodes, err := node.Parallel(parallelInputs)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create array of node objects
result := make([]interface{}, len(childNodes))
for i, childNode := range childNodes {
childNodeObj, err := NewNodeObject(ctx, childNode)
if err != nil {
continue
}
result[i] = childNodeObj
}
jsVal, err := bridge.JsValue(ctx, result)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}
func nodeSetOutputMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "SetOutput requires an output argument")
}
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := node.SetOutput(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func nodeSetMetadataMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "SetMetadata requires 2 arguments: (key, value)")
}
key := args[0].String()
value, err := bridge.GoValue(args[1], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := node.SetMetadata(key, value); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func nodeCompleteMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Optional output parameter
if len(args) > 0 {
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := node.Complete(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
} else {
if err := node.Complete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
}
return info.This().Value
})
}
func nodeFailMethod(iso *v8go.Isolate, node types.Node) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Fail requires an error message")
}
errMsg := args[0].String()
if err := node.Fail(fmt.Errorf("%s", errMsg)); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}

142
trace/jsapi/space.go Normal file
View file

@ -0,0 +1,142 @@
package jsapi
import (
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
// NewSpaceObject creates a JavaScript Space object (pure JS object, no Go mapping)
// Since TraceSpace is a struct and not an interface, we use manager methods to operate on it
func NewSpaceObject(v8ctx *v8go.Context, manager types.Manager, space *types.TraceSpace) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set primitive fields
jsObject.Set("id", space.ID)
// Set methods - they operate through manager
jsObject.Set("Set", spaceSetMethod(v8ctx.Isolate(), manager, space.ID))
jsObject.Set("Get", spaceGetMethod(v8ctx.Isolate(), manager, space.ID))
jsObject.Set("Has", spaceHasMethod(v8ctx.Isolate(), manager, space.ID))
jsObject.Set("Delete", spaceDeleteMethod(v8ctx.Isolate(), manager, space.ID))
jsObject.Set("Clear", spaceClearMethod(v8ctx.Isolate(), manager, space.ID))
jsObject.Set("Keys", spaceKeysMethod(v8ctx.Isolate(), manager, space.ID))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
return nil, err
}
return instance.Value, nil
}
// Space method templates
func spaceSetMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "Set requires 2 arguments: (key, value)")
}
key := args[0].String()
value, err := bridge.GoValue(args[1], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetSpaceValue(spaceID, key, value); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func spaceGetMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Get requires a key argument")
}
key := args[0].String()
value, err := manager.GetSpaceValue(spaceID, key)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
jsVal, err := bridge.JsValue(ctx, value)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}
func spaceHasMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Has requires a key argument")
}
key := args[0].String()
has := manager.HasSpaceValue(spaceID, key)
jsVal, _ := v8go.NewValue(iso, has)
return jsVal
})
}
func spaceDeleteMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Delete requires a key argument")
}
key := args[0].String()
if err := manager.DeleteSpaceValue(spaceID, key); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func spaceClearMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
if err := manager.ClearSpaceValues(spaceID); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func spaceKeysMethod(iso *v8go.Isolate, manager types.Manager, spaceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
keys := manager.ListSpaceKeys(spaceID)
jsVal, err := bridge.JsValue(ctx, keys)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}

506
trace/jsapi/trace.go Normal file
View file

@ -0,0 +1,506 @@
package jsapi
import (
"context"
"fmt"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/trace"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
func init() {
// Auto-register Trace JavaScript API when package is imported
v8.RegisterFunction("Trace", ExportFunction)
}
// NewTraceObject creates a JavaScript Trace object
func NewTraceObject(v8ctx *v8go.Context, traceID string, manager types.Manager) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set internal field count to 1 to store the __go_id
// Internal fields are not accessible from JavaScript, providing better security
jsObject.SetInternalFieldCount(1)
// Register manager in global bridge registry for efficient Go object retrieval
// The goValueID will be stored in internal field (index 0) after instance creation
// Internal fields are not accessible from JavaScript, providing better security
goValueID := bridge.RegisterGoObject(manager)
// Set primitive fields
jsObject.Set("id", traceID)
// Set release function that will be called when JavaScript object is released
// This function retrieves goValueID from internal field and releases the Go object
jsObject.Set("__release", traceGoRelease(v8ctx.Isolate(), traceID))
// Set methods
jsObject.Set("Add", traceAddMethod(v8ctx.Isolate(), manager))
jsObject.Set("Parallel", traceParallelMethod(v8ctx.Isolate(), manager))
jsObject.Set("Info", traceInfoMethod(v8ctx.Isolate(), manager))
jsObject.Set("Debug", traceDebugMethod(v8ctx.Isolate(), manager))
jsObject.Set("Error", traceErrorMethod(v8ctx.Isolate(), manager))
jsObject.Set("Warn", traceWarnMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetOutput", traceSetOutputMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetMetadata", traceSetMetadataMethod(v8ctx.Isolate(), manager))
jsObject.Set("Complete", traceCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("Fail", traceFailMethod(v8ctx.Isolate(), manager))
jsObject.Set("MarkComplete", traceMarkCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("CreateSpace", traceCreateSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("GetSpace", traceGetSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("IsComplete", traceIsCompleteMethod(v8ctx.Isolate(), manager))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
// Clean up: release from global registry if instance creation failed
bridge.ReleaseGoObject(goValueID)
return nil, err
}
// Store the goValueID in internal field (index 0)
// This is not accessible from JavaScript, providing better security
obj, err := instance.Value.AsObject()
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
err = obj.SetInternalField(0, goValueID)
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
return instance.Value, nil
}
// TraceNew creates a new Trace instance from JavaScript
// Usage: new Trace(options)
func TraceNew(v8ctx *v8go.Context, options map[string]interface{}) (*v8go.Value, error) {
cfg := config.Conf
// Prepare driver options
var driverOptions []any
var driverType string
// Allow override from options
driver, _ := options["driver"].(string)
if driver == "" {
driver = cfg.Trace.Driver
}
switch driver {
case "store":
driverType = trace.Store
storeID := cfg.Trace.Store
if sid, ok := options["store"].(string); ok && sid != "" {
storeID = sid
}
prefix := cfg.Trace.Prefix
if pfx, ok := options["prefix"].(string); ok {
prefix = pfx
}
driverOptions = []any{storeID, prefix}
case "local", "":
driverType = trace.Local
path := cfg.Trace.Path
if p, ok := options["path"].(string); ok && p != "" {
path = p
}
driverOptions = []any{path}
default:
return nil, fmt.Errorf("unsupported trace driver: %s", driver)
}
// Parse trace options
var traceOpt types.TraceOption
if id, ok := options["id"].(string); ok {
traceOpt.ID = id
}
// Create trace
goCtx := context.Background()
traceID, manager, err := trace.New(goCtx, driverType, &traceOpt, driverOptions...)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// TraceLoad loads an existing trace from JavaScript
// Usage: Trace.Load(traceID)
func TraceLoad(v8ctx *v8go.Context, traceID string) (*v8go.Value, error) {
manager, err := trace.Load(traceID)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// traceGoRelease releases the Go object from the global bridge registry
// It retrieves the goValueID from internal field (index 0) and releases the Go object
// It also calls trace.Release to cleanup the trace globally
func traceGoRelease(iso *v8go.Isolate, traceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
// Get the trace object (this)
thisObj, err := info.This().AsObject()
if err == nil && thisObj.InternalFieldCount() > 0 {
// Get goValueID from internal field (index 0)
goValueIDValue := thisObj.GetInternalField(0)
if goValueIDValue != nil && goValueIDValue.IsString() {
goValueID := goValueIDValue.String()
// Release from global bridge registry
bridge.ReleaseGoObject(goValueID)
}
}
// Call global trace.Release to remove from registry and stop background goroutines
trace.Release(traceID)
return v8go.Undefined(info.Context().Isolate())
})
}
// Method templates
func traceAddMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Add requires at least 1 argument: (input, option?)")
}
// Parse input
input, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid input: %s", err))
}
// Parse option
var option types.TraceNodeOption
if len(args) > 1 && !args[1].IsNullOrUndefined() {
optionObj, err := args[1].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.Add
node, err := manager.Add(input, option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create node JS object
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return nodeObj
})
}
func traceParallelMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Parallel requires an array argument")
}
// Parse array
parallelInputsJS, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid parallel inputs: %s", err))
}
// Convert to []types.TraceParallelInput
parallelInputsArray, ok := parallelInputsJS.([]interface{})
if !ok {
return bridge.JsException(ctx, "Parallel argument must be an array")
}
parallelInputs := make([]types.TraceParallelInput, 0, len(parallelInputsArray))
for _, item := range parallelInputsArray {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
parallelInput := types.TraceParallelInput{}
if input, ok := itemMap["input"]; ok {
parallelInput.Input = input
}
if option, ok := itemMap["option"].(map[string]interface{}); ok {
if label, ok := option["label"].(string); ok {
parallelInput.Option.Label = label
}
if icon, ok := option["icon"].(string); ok {
parallelInput.Option.Icon = icon
}
if desc, ok := option["description"].(string); ok {
parallelInput.Option.Description = desc
}
}
parallelInputs = append(parallelInputs, parallelInput)
}
// Call manager.Parallel
nodes, err := manager.Parallel(parallelInputs)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create array of node objects
result := make([]interface{}, len(nodes))
for i, node := range nodes {
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
continue
}
result[i] = nodeObj
}
jsVal, err := bridge.JsValue(ctx, result)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}
func traceInfoMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Info(args[0].String())
}
return info.This().Value
})
}
func traceDebugMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Debug(args[0].String())
}
return info.This().Value
})
}
func traceErrorMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Error(args[0].String())
}
return info.This().Value
})
}
func traceWarnMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Warn(args[0].String())
}
return info.This().Value
})
}
func traceSetOutputMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "SetOutput requires an output argument")
}
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetOutput(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceSetMetadataMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "SetMetadata requires 2 arguments: (key, value)")
}
key := args[0].String()
value, err := bridge.GoValue(args[1], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetMetadata(key, value); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Optional output parameter
if len(args) > 0 && !args[0].IsNullOrUndefined() {
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.Complete(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
} else {
if err := manager.Complete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
}
return info.This().Value
})
}
func traceFailMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Fail requires an error message")
}
errMsg := args[0].String()
if err := manager.Fail(fmt.Errorf("%s", errMsg)); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceMarkCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
if err := manager.MarkComplete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCreateSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Parse option
var option types.TraceSpaceOption
if len(args) > 0 && !args[0].IsNullOrUndefined() {
optionObj, err := args[0].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.CreateSpace
space, err := manager.CreateSpace(option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceGetSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "GetSpace requires a space ID")
}
spaceID := args[0].String()
space, err := manager.GetSpace(spaceID)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if space == nil {
return v8go.Null(iso)
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceIsCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
isComplete := manager.IsComplete()
jsVal, _ := v8go.NewValue(iso, isComplete)
return jsVal
})
}