package app import ( "context" "math/rand" "sync" "time" "github.com/dop251/goja" "github.com/dop251/goja_nodejs/eventloop" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) var ( ErrFuncDoesNotExist = errors.New("function does not exist") ErrUnknownError = errors.New("unknown error") ) type Server struct { loop *eventloop.EventLoop factories []ServerModuleFactory modules []ServerModule } func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...any) (any, error) { ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args)) ret, err := s.Exec(ctx, funcName, args...) if err != nil { return nil, errors.WithStack(err) } return ret, nil } func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...any) (any, error) { type result struct { value any err error } done := make(chan result) defer func() { // Drain done channel for range done { } }() s.loop.RunOnLoop(func(rt *goja.Runtime) { defer close(done) var callable goja.Callable switch typ := callableOrFuncname.(type) { case goja.Callable: callable = typ case string: call, ok := goja.AssertFunction(rt.Get(typ)) if !ok { done <- result{ err: errors.WithStack(ErrFuncDoesNotExist), } return } callable = call default: done <- result{ err: errors.Errorf("callableOrFuncname: expected callable or function name, got '%T'", callableOrFuncname), } return } defer func() { recovered := recover() if recovered == nil { return } recoveredErr, ok := recovered.(error) if !ok { panic(recovered) } done <- result{ err: recoveredErr, } }() jsArgs := make([]goja.Value, 0, len(args)) for _, a := range args { jsArgs = append(jsArgs, rt.ToValue(a)) } logger.Debug(ctx, "executing callable", logger.F("callable", callableOrFuncname)) start := time.Now() value, err := callable(nil, jsArgs...) if err != nil { done <- result{ err: errors.WithStack(err), } return } done <- result{ value: value.Export(), } logger.Debug(ctx, "executed callable", logger.F("callable", callableOrFuncname), logger.F("duration", time.Since(start).String())) }) select { case <-ctx.Done(): if err := ctx.Err(); err != nil { return nil, errors.WithStack(err) } return nil, nil case result := <-done: if result.err != nil { return nil, errors.WithStack(result.err) } if promise, ok := isPromise(result.value); ok { return s.waitForPromise(promise), nil } return result.value, nil } } func (s *Server) waitForPromise(promise *goja.Promise) any { var ( wg sync.WaitGroup value any ) wg.Add(1) // Wait for promise completion go func() { for { var loopWait sync.WaitGroup loopWait.Add(1) breakLoop := false s.loop.RunOnLoop(func(vm *goja.Runtime) { defer loopWait.Done() if promise.State() == goja.PromiseStatePending { return } value = promise.Result().Export() breakLoop = true }) loopWait.Wait() if breakLoop { wg.Done() return } } }() wg.Wait() return value } func (s *Server) Start(ctx context.Context, name string, src string) error { s.loop.Start() done := make(chan error) s.loop.RunOnLoop(func(rt *goja.Runtime) { defer close(done) rt.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true)) rt.SetRandSource(createRandomSource()) if err := s.loadModules(ctx, rt); err != nil { err = errors.WithStack(err) done <- err return } if _, err := rt.RunScript(name, src); err != nil { done <- errors.Wrap(err, "could not run js script") return } if err := s.initModules(ctx, rt); err != nil { err = errors.WithStack(err) done <- err return } done <- nil }) if err := <-done; err != nil { return errors.WithStack(err) } return nil } func (s *Server) Stop() { s.loop.Stop() } func (s *Server) loadModules(ctx context.Context, rt *goja.Runtime) error { modules := make([]ServerModule, 0, len(s.factories)) for _, moduleFactory := range s.factories { mod := moduleFactory(s) export := rt.NewObject() mod.Export(export) rt.Set(mod.Name(), export) modules = append(modules, mod) } s.modules = modules return nil } func (s *Server) initModules(ctx context.Context, rt *goja.Runtime) error { for _, mod := range s.modules { initMod, ok := mod.(InitializableModule) if !ok { continue } logger.Debug(ctx, "initializing module", logger.F("module", initMod.Name())) if err := initMod.OnInit(ctx, rt); err != nil { return errors.WithStack(err) } } return nil } func NewServer(factories ...ServerModuleFactory) *Server { server := &Server{ factories: factories, loop: eventloop.NewEventLoop( eventloop.EnableConsole(false), ), } return server } func createRandomSource() goja.RandSource { rnd := rand.New(&cryptoSource{}) return rnd.Float64 }