edge/pkg/app/server.go

279 lines
4.9 KiB
Go

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
}