node build fixed
This commit is contained in:
41
seanime-2.9.10/internal/util/goja/async.go
Normal file
41
seanime-2.9.10/internal/util/goja/async.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package goja_util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// BindAwait binds the $await function to the Goja runtime.
|
||||
// Hooks don't wait for promises to resolve, so $await is used to wrap a promise and wait for it to resolve.
|
||||
func BindAwait(vm *goja.Runtime) {
|
||||
vm.Set("$await", func(promise goja.Value) (goja.Value, error) {
|
||||
if promise, ok := promise.Export().(*goja.Promise); ok {
|
||||
doneCh := make(chan struct{})
|
||||
|
||||
// Wait for the promise to resolve
|
||||
go func() {
|
||||
for promise.State() == goja.PromiseStatePending {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
close(doneCh)
|
||||
}()
|
||||
|
||||
<-doneCh
|
||||
|
||||
// If the promise is rejected, return the error
|
||||
if promise.State() == goja.PromiseStateRejected {
|
||||
err := promise.Result()
|
||||
return nil, fmt.Errorf("promise rejected: %v", err)
|
||||
}
|
||||
|
||||
// If the promise is fulfilled, return the result
|
||||
res := promise.Result()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// If the promise is not a Goja promise, return the value as is
|
||||
return promise, nil
|
||||
})
|
||||
}
|
||||
228
seanime-2.9.10/internal/util/goja/mutable.go
Normal file
228
seanime-2.9.10/internal/util/goja/mutable.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package goja_util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func BindMutable(vm *goja.Runtime) {
|
||||
vm.Set("$mutable", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) == 0 || goja.IsUndefined(call.Arguments[0]) || goja.IsNull(call.Arguments[0]) {
|
||||
return vm.NewObject()
|
||||
}
|
||||
|
||||
// Convert the input to a map first
|
||||
jsonBytes, err := json.Marshal(call.Arguments[0].Export())
|
||||
if err != nil {
|
||||
panic(vm.NewTypeError("Failed to marshal input: %v", err))
|
||||
}
|
||||
|
||||
var objMap map[string]interface{}
|
||||
if err := json.Unmarshal(jsonBytes, &objMap); err != nil {
|
||||
panic(vm.NewTypeError("Failed to unmarshal input: %v", err))
|
||||
}
|
||||
|
||||
// Create a new object with getters and setters
|
||||
obj := vm.NewObject()
|
||||
|
||||
for key, val := range objMap {
|
||||
// Capture current key and value
|
||||
k, v := key, val
|
||||
|
||||
if mapVal, ok := v.(map[string]interface{}); ok {
|
||||
// For nested objects, create a new mutable object
|
||||
nestedObj := vm.NewObject()
|
||||
|
||||
// Add get method
|
||||
nestedObj.Set("get", vm.ToValue(func() interface{} {
|
||||
return mapVal
|
||||
}))
|
||||
|
||||
// Add set method
|
||||
nestedObj.Set("set", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) > 0 {
|
||||
newVal := call.Arguments[0].Export()
|
||||
if newMap, ok := newVal.(map[string]interface{}); ok {
|
||||
mapVal = newMap
|
||||
objMap[k] = newMap
|
||||
}
|
||||
}
|
||||
return goja.Undefined()
|
||||
}))
|
||||
|
||||
// Add direct property access
|
||||
for mk, mv := range mapVal {
|
||||
// Capture map key and value
|
||||
mapKey := mk
|
||||
mapValue := mv
|
||||
nestedObj.DefineAccessorProperty(mapKey, vm.ToValue(func() interface{} {
|
||||
return mapValue
|
||||
}), vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) > 0 {
|
||||
mapVal[mapKey] = call.Arguments[0].Export()
|
||||
}
|
||||
return goja.Undefined()
|
||||
}), goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
|
||||
obj.Set(k, nestedObj)
|
||||
} else if arrVal, ok := v.([]interface{}); ok {
|
||||
// For arrays, create a proxy object that allows index access
|
||||
arrObj := vm.NewObject()
|
||||
for i, av := range arrVal {
|
||||
idx := i
|
||||
val := av
|
||||
arrObj.DefineAccessorProperty(fmt.Sprintf("%d", idx), vm.ToValue(func() interface{} {
|
||||
return val
|
||||
}), vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) > 0 {
|
||||
arrVal[idx] = call.Arguments[0].Export()
|
||||
objMap[k] = arrVal
|
||||
}
|
||||
return goja.Undefined()
|
||||
}), goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
arrObj.Set("length", len(arrVal))
|
||||
|
||||
// Add explicit get/set methods for arrays
|
||||
arrObj.Set("get", vm.ToValue(func() interface{} {
|
||||
return arrVal
|
||||
}))
|
||||
arrObj.Set("set", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) > 0 {
|
||||
newVal := call.Arguments[0].Export()
|
||||
if newArr, ok := newVal.([]interface{}); ok {
|
||||
arrVal = newArr
|
||||
objMap[k] = newArr
|
||||
arrObj.Set("length", len(newArr))
|
||||
}
|
||||
}
|
||||
return goja.Undefined()
|
||||
}))
|
||||
obj.Set(k, arrObj)
|
||||
} else {
|
||||
// For primitive values, create simple getter/setter
|
||||
obj.DefineAccessorProperty(k, vm.ToValue(func() interface{} {
|
||||
return objMap[k]
|
||||
}), vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) > 0 {
|
||||
objMap[k] = call.Arguments[0].Export()
|
||||
}
|
||||
return goja.Undefined()
|
||||
}), goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
}
|
||||
|
||||
// Add a toJSON method that creates a fresh copy
|
||||
obj.Set("toJSON", vm.ToValue(func() interface{} {
|
||||
// Convert to JSON and back to create a fresh copy with no shared references
|
||||
jsonBytes, err := json.Marshal(objMap)
|
||||
if err != nil {
|
||||
panic(vm.NewTypeError("Failed to marshal to JSON: %v", err))
|
||||
}
|
||||
|
||||
var freshCopy interface{}
|
||||
if err := json.Unmarshal(jsonBytes, &freshCopy); err != nil {
|
||||
panic(vm.NewTypeError("Failed to unmarshal from JSON: %v", err))
|
||||
}
|
||||
|
||||
return freshCopy
|
||||
}))
|
||||
|
||||
// Add a replace method to completely replace a Go struct's contents.
|
||||
// Usage in JS: mutableAnime.replace(e.anime)
|
||||
obj.Set("replace", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
panic(vm.NewTypeError("replace requires one argument: target"))
|
||||
}
|
||||
|
||||
// Use the current internal state.
|
||||
jsonBytes, err := json.Marshal(objMap)
|
||||
if err != nil {
|
||||
panic(vm.NewTypeError("Failed to marshal state: %v", err))
|
||||
}
|
||||
|
||||
// Get the reflect.Value of the target pointer
|
||||
target := call.Arguments[0].Export()
|
||||
targetVal := reflect.ValueOf(target)
|
||||
if targetVal.Kind() != reflect.Ptr {
|
||||
// panic(vm.NewTypeError("Target must be a pointer"))
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// Create a new instance of the target type and unmarshal into it
|
||||
newVal := reflect.New(targetVal.Elem().Type())
|
||||
if err := json.Unmarshal(jsonBytes, newVal.Interface()); err != nil {
|
||||
panic(vm.NewTypeError("Failed to unmarshal into target: %v", err))
|
||||
}
|
||||
|
||||
// Replace the contents of the target with the new value
|
||||
targetVal.Elem().Set(newVal.Elem())
|
||||
|
||||
return goja.Undefined()
|
||||
}))
|
||||
|
||||
return obj
|
||||
}))
|
||||
|
||||
// Add replace function to completely replace a Go struct's contents
|
||||
vm.Set("$replace", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 2 {
|
||||
panic(vm.NewTypeError("replace requires two arguments: target and source"))
|
||||
}
|
||||
|
||||
target := call.Arguments[0].Export()
|
||||
source := call.Arguments[1].Export()
|
||||
|
||||
// Marshal source to JSON
|
||||
sourceJSON, err := json.Marshal(source)
|
||||
if err != nil {
|
||||
panic(vm.NewTypeError("Failed to marshal source: %v", err))
|
||||
}
|
||||
|
||||
// Get the reflect.Value of the target pointer
|
||||
targetVal := reflect.ValueOf(target)
|
||||
if targetVal.Kind() != reflect.Ptr {
|
||||
// panic(vm.NewTypeError("Target must be a pointer"))
|
||||
// TODO: Handle non-pointer targets
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// Create a new instance of the target type
|
||||
newVal := reflect.New(targetVal.Elem().Type())
|
||||
|
||||
// Unmarshal JSON into the new instance
|
||||
if err := json.Unmarshal(sourceJSON, newVal.Interface()); err != nil {
|
||||
panic(vm.NewTypeError("Failed to unmarshal into target: %v", err))
|
||||
}
|
||||
|
||||
// Replace the contents of the target with the new value
|
||||
targetVal.Elem().Set(newVal.Elem())
|
||||
|
||||
return goja.Undefined()
|
||||
}))
|
||||
|
||||
vm.Set("$clone", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) == 0 {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// First convert to JSON to strip all pointers and references
|
||||
jsonBytes, err := json.Marshal(call.Arguments[0].Export())
|
||||
if err != nil {
|
||||
panic(vm.NewTypeError("Failed to marshal input: %v", err))
|
||||
}
|
||||
|
||||
// Then unmarshal into a fresh interface{} to get a completely new object
|
||||
var newObj interface{}
|
||||
if err := json.Unmarshal(jsonBytes, &newObj); err != nil {
|
||||
panic(vm.NewTypeError("Failed to unmarshal input: %v", err))
|
||||
}
|
||||
|
||||
// Convert back to a goja value
|
||||
return vm.ToValue(newObj)
|
||||
}))
|
||||
}
|
||||
234
seanime-2.9.10/internal/util/goja/scheduler.go
Normal file
234
seanime-2.9.10/internal/util/goja/scheduler.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package goja_util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
// Job represents a task to be executed in the VM
|
||||
type Job struct {
|
||||
fn func() error
|
||||
resultCh chan error
|
||||
async bool // Flag to indicate if the job is async (doesn't need to wait for result)
|
||||
}
|
||||
|
||||
// Scheduler handles all VM operations added concurrently in a single goroutine
|
||||
// Any goroutine that needs to execute a VM operation must schedule it because the UI VM isn't thread safe
|
||||
type Scheduler struct {
|
||||
jobQueue chan *Job
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
// Track the currently executing job to detect nested scheduling
|
||||
currentJob *Job
|
||||
currentJobLock sync.Mutex
|
||||
|
||||
onException mo.Option[func(err error)]
|
||||
}
|
||||
|
||||
func NewScheduler() *Scheduler {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := &Scheduler{
|
||||
jobQueue: make(chan *Job, 9999),
|
||||
ctx: ctx,
|
||||
onException: mo.None[func(err error)](),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
s.start()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scheduler) SetOnException(onException func(err error)) {
|
||||
s.onException = mo.Some(onException)
|
||||
}
|
||||
|
||||
func (s *Scheduler) start() {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case job := <-s.jobQueue:
|
||||
// Set the current job before execution
|
||||
s.currentJobLock.Lock()
|
||||
s.currentJob = job
|
||||
s.currentJobLock.Unlock()
|
||||
|
||||
err := job.fn()
|
||||
|
||||
// Clear the current job after execution
|
||||
s.currentJobLock.Lock()
|
||||
s.currentJob = nil
|
||||
s.currentJobLock.Unlock()
|
||||
|
||||
// Only send result if the job is not async
|
||||
if !job.async {
|
||||
job.resultCh <- err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if onException, ok := s.onException.Get(); ok {
|
||||
onException(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Scheduler) Stop() {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
//s.wg.Wait()
|
||||
}
|
||||
|
||||
// Schedule adds a job to the queue and waits for its completion
|
||||
func (s *Scheduler) Schedule(fn func() error) error {
|
||||
resultCh := make(chan error, 1)
|
||||
job := &Job{
|
||||
fn: func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resultCh <- fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
},
|
||||
resultCh: resultCh,
|
||||
async: false,
|
||||
}
|
||||
|
||||
// Check if we're already in a job execution context
|
||||
s.currentJobLock.Lock()
|
||||
isNestedCall := s.currentJob != nil && !s.currentJob.async
|
||||
s.currentJobLock.Unlock()
|
||||
|
||||
// If this is a nested call from a synchronous job, we need to be careful
|
||||
// We can't execute directly because the VM isn't thread-safe
|
||||
// Instead, we'll queue it and use a separate goroutine to wait for the result
|
||||
if isNestedCall {
|
||||
// Queue the job
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return fmt.Errorf("scheduler stopped")
|
||||
case s.jobQueue <- job:
|
||||
// Create a separate goroutine to wait for the result
|
||||
// This prevents deadlock while still ensuring the job runs in the scheduler
|
||||
resultCh2 := make(chan error, 1)
|
||||
go func() {
|
||||
resultCh2 <- <-resultCh
|
||||
}()
|
||||
return <-resultCh2
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, queue the job normally
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return fmt.Errorf("scheduler stopped")
|
||||
case s.jobQueue <- job:
|
||||
return <-resultCh
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleAsync adds a job to the queue without waiting for completion
|
||||
// This is useful for fire-and-forget operations or when a job needs to schedule another job
|
||||
func (s *Scheduler) ScheduleAsync(fn func() error) {
|
||||
job := &Job{
|
||||
fn: func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Get stack trace for better identification
|
||||
stack := debug.Stack()
|
||||
jobInfo := fmt.Sprintf("async job panic: %v\nStack: %s", r, stack)
|
||||
|
||||
if onException, ok := s.onException.Get(); ok {
|
||||
onException(fmt.Errorf("panic in async job: %v\n%s", r, jobInfo))
|
||||
}
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
},
|
||||
resultCh: nil, // No result channel needed
|
||||
async: true,
|
||||
}
|
||||
// Queue the job without blocking
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
// Scheduler is stopped, just ignore
|
||||
return
|
||||
case s.jobQueue <- job:
|
||||
// Job queued successfully
|
||||
// fmt.Printf("job queued successfully, length: %d\n", len(s.jobQueue))
|
||||
return
|
||||
default:
|
||||
// Queue is full, log an error
|
||||
if onException, ok := s.onException.Get(); ok {
|
||||
onException(fmt.Errorf("async job queue is full"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleWithTimeout schedules a job with a timeout
|
||||
func (s *Scheduler) ScheduleWithTimeout(fn func() error, timeout time.Duration) error {
|
||||
resultCh := make(chan error, 1)
|
||||
job := &Job{
|
||||
fn: func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resultCh <- fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
},
|
||||
resultCh: resultCh,
|
||||
async: false,
|
||||
}
|
||||
|
||||
// Check if we're already in a job execution context
|
||||
s.currentJobLock.Lock()
|
||||
isNestedCall := s.currentJob != nil && !s.currentJob.async
|
||||
s.currentJobLock.Unlock()
|
||||
|
||||
// If this is a nested call from a synchronous job, handle it specially
|
||||
if isNestedCall {
|
||||
// Queue the job
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return fmt.Errorf("scheduler stopped")
|
||||
case s.jobQueue <- job:
|
||||
// Create a separate goroutine to wait for the result with timeout
|
||||
resultCh2 := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
resultCh2 <- err
|
||||
case <-time.After(timeout):
|
||||
resultCh2 <- fmt.Errorf("operation timed out")
|
||||
}
|
||||
}()
|
||||
return <-resultCh2
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return fmt.Errorf("scheduler stopped")
|
||||
case s.jobQueue <- job:
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf("operation timed out")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user