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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/yaoapp/gou/plan"
|
"github.com/yaoapp/gou/plan"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
"github.com/yaoapp/yao/trace"
|
||||||
|
traceTypes "github.com/yaoapp/yao/trace/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New create a new context
|
// New create a new context
|
||||||
|
|
@ -62,8 +66,19 @@ func WithTimeout(parent Context, timeout time.Duration) (Context, context.Cancel
|
||||||
return parent, 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() {
|
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
|
// Clear space
|
||||||
if ctx.Space != nil {
|
if ctx.Space != nil {
|
||||||
ctx.Space.Clear()
|
ctx.Space.Clear()
|
||||||
|
|
@ -98,6 +113,70 @@ func (ctx *Context) Send(data []byte) error {
|
||||||
return err
|
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
|
// Map the context to a map
|
||||||
func (ctx *Context) Map() map[string]interface{} {
|
func (ctx *Context) Map() map[string]interface{} {
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/yaoapp/gou/runtime/v8/bridge"
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||||
|
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
||||||
"rogchap.com/v8go"
|
"rogchap.com/v8go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var objectsMutex = sync.Mutex{}
|
|
||||||
var objects = map[string]*Context{}
|
|
||||||
|
|
||||||
// JsValue return the JavaScript value of the context
|
// JsValue return the JavaScript value of the context
|
||||||
func (ctx *Context) JsValue(v8ctx *v8go.Context) (*v8go.Value, error) {
|
func (ctx *Context) JsValue(v8ctx *v8go.Context) (*v8go.Value, error) {
|
||||||
return ctx.NewObject(v8ctx)
|
return ctx.NewObject(v8ctx)
|
||||||
|
|
@ -21,10 +16,16 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
|
||||||
|
|
||||||
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
|
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
|
||||||
|
|
||||||
// Set the id and release function
|
// Set internal field count to 1 to store the goValueID
|
||||||
id := ctx.objectRegister()
|
// Internal fields are not accessible from JavaScript, providing better security
|
||||||
jsObject.Set("__id", id)
|
jsObject.SetInternalFieldCount(1)
|
||||||
jsObject.Set("__release", ctx.objectRelease(v8ctx.Isolate(), id))
|
|
||||||
|
// 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
|
// Set primitive fields in template
|
||||||
jsObject.Set("chat_id", ctx.ChatID)
|
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("accept", string(ctx.Accept))
|
||||||
jsObject.Set("route", ctx.Route)
|
jsObject.Set("route", ctx.Route)
|
||||||
|
|
||||||
|
// Set methods
|
||||||
|
jsObject.Set("Trace", ctx.traceMethod(v8ctx.Isolate()))
|
||||||
|
|
||||||
// Create instance
|
// Create instance
|
||||||
instance, err := jsObject.NewInstance(v8ctx)
|
instance, err := jsObject.NewInstance(v8ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.objectRelease(v8ctx.Isolate(), id)
|
// Clean up: release from global registry if instance creation failed
|
||||||
|
bridge.ReleaseGoObject(goValueID)
|
||||||
return nil, err
|
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()
|
obj, err := instance.Value.AsObject()
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,19 +111,49 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
|
||||||
return instance.Value, nil
|
return instance.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) objectRegister() string {
|
// objectRelease releases the Go object from the global bridge registry
|
||||||
id := uuid.NewString()
|
// It retrieves the goValueID from internal field (index 0) and releases the Go object
|
||||||
objectsMutex.Lock()
|
func (ctx *Context) objectRelease(iso *v8go.Isolate, goValueID string) *v8go.FunctionTemplate {
|
||||||
defer objectsMutex.Unlock()
|
|
||||||
objects[id] = ctx
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) objectRelease(iso *v8go.Isolate, id string) *v8go.FunctionTemplate {
|
|
||||||
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
objectsMutex.Lock()
|
// Get the context object (this)
|
||||||
defer objectsMutex.Unlock()
|
thisObj, err := info.This().AsObject()
|
||||||
delete(objects, id)
|
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())
|
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
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -35,7 +36,7 @@ func TestJsValue(t *testing.T) {
|
||||||
t.Fatalf("Call failed: %v", err)
|
t.Fatalf("Call failed: %v", err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, "ChatID-123456", res)
|
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 {
|
func testContextJsvalueEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
|
@ -132,7 +133,7 @@ func TestJsValueConcurrent(t *testing.T) {
|
||||||
|
|
||||||
// Verify all objects are cleaned up after GC
|
// Verify all objects are cleaned up after GC
|
||||||
// Note: objects should be released when v8 values are garbage collected
|
// 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
|
// TestJsValueRegistrationAndCleanup test the object registration and cleanup mechanism
|
||||||
|
|
@ -141,11 +142,6 @@ func TestJsValueRegistrationAndCleanup(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
// Clear objects map before test
|
|
||||||
objectsMutex.Lock()
|
|
||||||
objects = map[string]*Context{}
|
|
||||||
objectsMutex.Unlock()
|
|
||||||
|
|
||||||
v8.RegisterFunction("testContextRegistration", testContextRegistrationEmbed)
|
v8.RegisterFunction("testContextRegistration", testContextRegistrationEmbed)
|
||||||
|
|
||||||
// Create multiple contexts and verify registration
|
// 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
|
// 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 {
|
func testContextRegistrationEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
|
@ -186,16 +182,6 @@ func testContextRegistrationFunction(info *v8go.FunctionCallbackInfo) *v8go.Valu
|
||||||
return bridge.JsException(info.Context(), err)
|
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
|
// Verify the object has __release function
|
||||||
release, err := ctx.Get("__release")
|
release, err := ctx.Get("__release")
|
||||||
if err != nil {
|
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"))
|
return bridge.JsException(info.Context(), fmt.Errorf("__release should be a function"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the object is registered
|
// Verify the object has internal field (goValueID is stored in internal field, not accessible from JS)
|
||||||
objectsMutex.Lock()
|
if ctx.InternalFieldCount() == 0 {
|
||||||
idStr := id.String()
|
return bridge.JsException(info.Context(), fmt.Errorf("object should have internal field"))
|
||||||
_, exists := objects[idStr]
|
}
|
||||||
objectsMutex.Unlock()
|
|
||||||
|
|
||||||
if !exists {
|
goValueID := ctx.GetInternalField(0)
|
||||||
return bridge.JsException(info.Context(), fmt.Errorf("object %s not registered", idStr))
|
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)
|
val, err := v8go.NewValue(info.Context().Isolate(), true)
|
||||||
|
|
@ -348,7 +334,7 @@ func TestJsValueAllFields(t *testing.T) {
|
||||||
_, hasSilent := result["silent"]
|
_, hasSilent := result["silent"]
|
||||||
assert.False(t, hasSilent, "silent (deprecated) should not be exported")
|
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 {
|
func testAllFieldsEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
|
@ -442,3 +428,68 @@ func testAllFieldsFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
}
|
}
|
||||||
return jsVal
|
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/plan"
|
||||||
"github.com/yaoapp/gou/store"
|
"github.com/yaoapp/gou/store"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"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.
|
// 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.Context
|
context.Context
|
||||||
Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call
|
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"
|
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
|
Stack *Stack `json:"-"` // Stack, current active stack of the request
|
||||||
Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging)
|
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
|
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 information
|
||||||
Authorized *types.AuthorizedInfo `json:"authorized,omitempty"` // 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
|
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 设定为生产环境
|
// Production 设定为生产环境
|
||||||
func Production() {
|
func Production() {
|
||||||
os.Setenv("YAO_MODE", "production")
|
os.Setenv("YAO_MODE", "production")
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ type Config struct {
|
||||||
Session Session `json:"session,omitempty"` // Session Config
|
Session Session `json:"session,omitempty"` // Session Config
|
||||||
Studio Studio `json:"studio,omitempty"` // Studio config
|
Studio Studio `json:"studio,omitempty"` // Studio config
|
||||||
Runtime Runtime `json:"runtime,omitempty"` // Runtime config
|
Runtime Runtime `json:"runtime,omitempty"` // Runtime config
|
||||||
|
Trace Trace `json:"trace,omitempty"` // Trace config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Studio the studio 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
|
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.
|
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/helper"
|
||||||
_ "github.com/yaoapp/yao/openai"
|
_ "github.com/yaoapp/yao/openai"
|
||||||
_ "github.com/yaoapp/yao/seed"
|
_ "github.com/yaoapp/yao/seed"
|
||||||
|
_ "github.com/yaoapp/yao/trace/jsapi"
|
||||||
_ "github.com/yaoapp/yao/wework"
|
_ "github.com/yaoapp/yao/wework"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/cmd"
|
"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