feat: initial commit
This commit is contained in:
16
pkg/app/app.go
Normal file
16
pkg/app/app.go
Normal file
@ -0,0 +1,16 @@
|
||||
package app
|
||||
|
||||
type ID string
|
||||
|
||||
type Manifest struct {
|
||||
ID ID `yaml:"id"`
|
||||
Version string `yaml:"version"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Tags []string `yaml:"tags"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ID ID
|
||||
Manifest *Manifest
|
||||
}
|
25
pkg/app/crypto.go
Normal file
25
pkg/app/crypto.go
Normal file
@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type cryptoSource struct{}
|
||||
|
||||
func (s cryptoSource) Seed(seed int64) {}
|
||||
|
||||
func (s cryptoSource) Int63() int64 {
|
||||
return int64(s.Uint64() & ^uint64(1<<63))
|
||||
}
|
||||
|
||||
func (s cryptoSource) Uint64() (v uint64) {
|
||||
err := binary.Read(rand.Reader, binary.BigEndian, &v)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not read number for random source"))
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
5
pkg/app/error.go
Normal file
5
pkg/app/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package app
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var ErrUnknownBundleArchiveFormat = errors.New("unknown bundle archive format")
|
101
pkg/app/loader.go
Normal file
101
pkg/app/loader.go
Normal file
@ -0,0 +1,101 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type FilesystemLoader struct {
|
||||
searchPatterns []string
|
||||
}
|
||||
|
||||
type LoadedApp struct {
|
||||
App *App
|
||||
Bundle bundle.Bundle
|
||||
}
|
||||
|
||||
func (l *FilesystemLoader) Load(ctx context.Context) ([]*LoadedApp, error) {
|
||||
apps := make([]*LoadedApp, 0)
|
||||
|
||||
for _, seachPattern := range l.searchPatterns {
|
||||
absSearchPattern, err := filepath.Abs(seachPattern)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not generate absolute path for '%s'", seachPattern)
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "searching apps in filesystem", logger.F("searchPattern", absSearchPattern))
|
||||
|
||||
files, err := filepath.Glob(absSearchPattern)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not search files with pattern '%s'", absSearchPattern)
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
loopCtx := logger.With(ctx, logger.F("file", f))
|
||||
|
||||
logger.Debug(loopCtx, "found app bundle")
|
||||
|
||||
b, err := bundle.FromPath(f)
|
||||
if err != nil {
|
||||
logger.Error(loopCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(loopCtx, "loading app manifest")
|
||||
|
||||
appManifest, err := LoadAppManifest(b)
|
||||
if err != nil {
|
||||
logger.Error(loopCtx, "could not load app manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
g := &App{
|
||||
ID: appManifest.ID,
|
||||
Manifest: appManifest,
|
||||
}
|
||||
|
||||
apps = append(apps, &LoadedApp{
|
||||
App: g,
|
||||
Bundle: b,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func NewFilesystemLoader(searchPatterns ...string) *FilesystemLoader {
|
||||
return &FilesystemLoader{
|
||||
searchPatterns: searchPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
func LoadAppManifest(b bundle.Bundle) (*Manifest, error) {
|
||||
reader, _, err := b.File("manifest.yml")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not read manifest.yml")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
manifest := &Manifest{}
|
||||
|
||||
decoder := yaml.NewDecoder(reader)
|
||||
if err := decoder.Decode(manifest); err != nil {
|
||||
return nil, errors.Wrap(err, "could not decode manifest.yml")
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
41
pkg/app/promise_proxy.go
Normal file
41
pkg/app/promise_proxy.go
Normal file
@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type PromiseProxy struct {
|
||||
*goja.Promise
|
||||
wg sync.WaitGroup
|
||||
resolve func(result interface{})
|
||||
reject func(reason interface{})
|
||||
}
|
||||
|
||||
func (p *PromiseProxy) Resolve(result interface{}) {
|
||||
defer p.wg.Done()
|
||||
p.resolve(result)
|
||||
}
|
||||
|
||||
func (p *PromiseProxy) Reject(reason interface{}) {
|
||||
defer p.wg.Done()
|
||||
p.resolve(reason)
|
||||
}
|
||||
|
||||
func (p *PromiseProxy) Wait() {
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
func NewPromiseProxy(promise *goja.Promise, resolve func(result interface{}), reject func(reason interface{})) *PromiseProxy {
|
||||
proxy := &PromiseProxy{
|
||||
Promise: promise,
|
||||
wg: sync.WaitGroup{},
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
}
|
||||
|
||||
proxy.wg.Add(1)
|
||||
|
||||
return proxy
|
||||
}
|
182
pkg/app/server.go
Normal file
182
pkg/app/server.go
Normal file
@ -0,0 +1,182 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrFuncDoesNotExist = errors.New("function does not exist")
|
||||
|
||||
type Server struct {
|
||||
runtime *goja.Runtime
|
||||
loop *eventloop.EventLoop
|
||||
modules []ServerModule
|
||||
}
|
||||
|
||||
func (s *Server) Load(name string, src string) error {
|
||||
_, err := s.runtime.RunScript(name, src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not run js script")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ExecFuncByName(funcName string, args ...interface{}) (goja.Value, error) {
|
||||
callable, ok := goja.AssertFunction(s.runtime.Get(funcName))
|
||||
if !ok {
|
||||
return nil, errors.WithStack(ErrFuncDoesNotExist)
|
||||
}
|
||||
|
||||
return s.Exec(callable, args...)
|
||||
}
|
||||
|
||||
func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value, error) {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
value goja.Value
|
||||
err error
|
||||
)
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
s.loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
jsArgs := make([]goja.Value, 0, len(args))
|
||||
for _, a := range args {
|
||||
jsArgs = append(jsArgs, vm.ToValue(a))
|
||||
}
|
||||
|
||||
value, err = callable(nil, jsArgs...)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return value, err
|
||||
}
|
||||
|
||||
func (s *Server) IsPromise(v goja.Value) (*goja.Promise, bool) {
|
||||
promise, ok := v.Export().(*goja.Promise)
|
||||
return promise, ok
|
||||
}
|
||||
|
||||
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) NewPromise() *PromiseProxy {
|
||||
promise, resolve, reject := s.runtime.NewPromise()
|
||||
|
||||
return NewPromiseProxy(promise, resolve, reject)
|
||||
}
|
||||
|
||||
func (s *Server) ToValue(v interface{}) goja.Value {
|
||||
return s.runtime.ToValue(v)
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.loop.Start()
|
||||
|
||||
for _, mod := range s.modules {
|
||||
initMod, ok := mod.(InitializableModule)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := initMod.OnInit(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
s.loop.Stop()
|
||||
}
|
||||
|
||||
func (s *Server) initModules(factories ...ServerModuleFactory) {
|
||||
runtime := goja.New()
|
||||
|
||||
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||
runtime.SetRandSource(createRandomSource())
|
||||
|
||||
modules := make([]ServerModule, 0, len(factories))
|
||||
|
||||
for _, moduleFactory := range factories {
|
||||
mod := moduleFactory(s)
|
||||
export := runtime.NewObject()
|
||||
mod.Export(export)
|
||||
runtime.Set(mod.Name(), export)
|
||||
|
||||
modules = append(modules, mod)
|
||||
}
|
||||
|
||||
s.runtime = runtime
|
||||
s.modules = modules
|
||||
}
|
||||
|
||||
func NewServer(factories ...ServerModuleFactory) *Server {
|
||||
server := &Server{
|
||||
loop: eventloop.NewEventLoop(
|
||||
eventloop.EnableConsole(false),
|
||||
),
|
||||
}
|
||||
|
||||
server.initModules(factories...)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func createRandomSource() goja.RandSource {
|
||||
rnd := rand.New(&cryptoSource{})
|
||||
|
||||
return rnd.Float64
|
||||
}
|
17
pkg/app/server_module.go
Normal file
17
pkg/app/server_module.go
Normal file
@ -0,0 +1,17 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type ServerModuleFactory func(*Server) ServerModule
|
||||
|
||||
type ServerModule interface {
|
||||
Name() string
|
||||
Export(*goja.Object)
|
||||
}
|
||||
|
||||
type InitializableModule interface {
|
||||
ServerModule
|
||||
OnInit() error
|
||||
}
|
Reference in New Issue
Block a user