package app import ( "context" "math/rand" "sync" "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) Load(name string, src string) error { var err error s.loop.RunOnLoop(func(rt *goja.Runtime) { _, err = rt.RunScript(name, src) if err != nil { err = errors.Wrap(err, "could not run js script") } }) if err != nil { return errors.WithStack(err) } return nil } func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...interface{}) (goja.Value, 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 ...interface{}) (goja.Value, error) { var ( wg sync.WaitGroup value goja.Value err error ) wg.Add(1) s.loop.RunOnLoop(func(rt *goja.Runtime) { var callable goja.Callable switch typ := callableOrFuncname.(type) { case goja.Callable: callable = typ case string: call, ok := goja.AssertFunction(rt.Get(typ)) if !ok { err = errors.WithStack(ErrFuncDoesNotExist) return } callable = call default: err = errors.Errorf("callableOrFuncname: expected callable or function name, got '%T'", callableOrFuncname) return } logger.Debug(ctx, "executing callable") defer wg.Done() defer func() { if recovered := recover(); recovered != nil { revoveredErr, ok := recovered.(error) if ok { logger.Error(ctx, "recovered runtime error", logger.E(errors.WithStack(revoveredErr))) err = errors.WithStack(ErrUnknownError) return } panic(recovered) } }() jsArgs := make([]goja.Value, 0, len(args)) for _, a := range args { jsArgs = append(jsArgs, rt.ToValue(a)) } value, err = callable(nil, jsArgs...) if err != nil { err = errors.WithStack(err) } }) wg.Wait() if err != nil { return nil, errors.WithStack(err) } return value, nil } func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value { var ( wg sync.WaitGroup value goja.Value ) 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() breakLoop = true }) loopWait.Wait() if breakLoop { wg.Done() return } } }() wg.Wait() return value } func (s *Server) Start(ctx context.Context) error { s.loop.Start() var err error s.loop.RunOnLoop(func(rt *goja.Runtime) { rt.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true)) rt.SetRandSource(createRandomSource()) if err = s.initModules(ctx, rt); err != nil { err = errors.WithStack(err) } }) if err != nil { return errors.WithStack(err) } return nil } func (s *Server) Stop() { s.loop.Stop() } func (s *Server) initModules(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) } for _, mod := range 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) } } s.modules = modules 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 }