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:
parent
251b0591a2
commit
c25fbb22d7
12 changed files with 1865 additions and 56 deletions
|
|
@ -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{}{}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
1
main.go
1
main.go
|
|
@ -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
83
trace/jsapi/jsapi.go
Normal 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
544
trace/jsapi/jsapi_test.go
Normal 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
308
trace/jsapi/node.go
Normal 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
142
trace/jsapi/space.go
Normal 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
506
trace/jsapi/trace.go
Normal 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
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue