Skip to content

Instantly share code, notes, and snippets.

@iKonrad
Created July 8, 2017 22:29
Show Gist options
  • Save iKonrad/a046de73c8e478b3cac07fa9e419af58 to your computer and use it in GitHub Desktop.
Save iKonrad/a046de73c8e478b3cac07fa9e419af58 to your computer and use it in GitHub Desktop.
package middleware
import (
crand "crypto/rand"
"encoding/binary"
"fmt"
"html/template"
"math/rand"
"net/http"
"runtime"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/fatih/structs"
"github.com/labstack/echo"
"github.com/nu7hatch/gouuid"
"github.com/olebedev/gojax/fetch"
"io/ioutil"
)
// React struct is contains JS vms
// pool to serve HTTP requests and
// separates some domain specific
// resources.
type React struct {
pool
debug bool
path string
}
var ReactJS *React
// NewReact initialized React struct
func NewReact(filePath string, debug bool, proxy http.Handler) *React {
r := &React{
debug: debug,
path: filePath,
}
if !debug {
fmt.Println("CPU", runtime.NumCPU())
r.pool = newEnginePool(filePath, runtime.NumCPU(), proxy)
} else {
// Use onDemandPool to load full react
// app each time for any http requests.
// Useful to debug the app.
r.pool = &onDemandPool{
path: filePath,
proxy: proxy,
}
}
return r
}
// Handle handles all HTTP requests which
// have not been caught via static file
// handler or other middlewares.
func (r *React) Handle(c echo.Context) error {
UUID := c.Get("uuid").(*uuid.UUID)
defer func() {
if r := recover(); r != nil {
c.Render(http.StatusInternalServerError, "react.html", Resp{
UUID: UUID.String(),
Error: r.(string),
})
}
}()
vm := r.get()
start := time.Now()
fmt.Println(c.Get("Store"))
params := map[string]interface{}{
"url": c.Request().URL.String(),
"headers": map[string][]string(c.Request().Header),
"uuid": UUID.String(),
"state": c.Get("State"),
"response": c.Get("Response"),
"meta": map[string]interface{}{
"lol": "test",
},
}
select {
case re := <-vm.Handle(params):
// Return vm back to the pool
r.put(vm)
re.RenderTime = time.Since(start)
// Handle the Response
if len(re.Redirect) == 0 && len(re.Error) == 0 {
// If no redirection and no errors
c.Response().Header().Set("X-React-Render-Time", re.RenderTime.String())
return c.Render(http.StatusOK, "react.html", re)
// If redirect
} else if len(re.Redirect) != 0 {
return c.Redirect(http.StatusMovedPermanently, re.Redirect)
// If internal error
} else if len(re.Error) != 0 {
c.Response().Header().Set ("X-React-Render-Time", re.RenderTime.String())
return c.Render(http.StatusInternalServerError, "react.html", re)
}
case <-time.After(2 * time.Second):
// release the context
r.drop(vm)
return c.Render(http.StatusInternalServerError, "react.html", Resp{
UUID: UUID.String(),
Error: "timeout",
})
}
return nil
}
// Resp is a struct for convinient
// react app Response parsing.
// Feel free to add any other keys to this struct
// and return value for this key at ecmascript side.
// Keep it sync with: src/app/client/router/toString.js:23
type Resp struct {
UUID string `json:"uuid"`
Error string `json:"error"`
Redirect string `json:"redirect"`
App string `json:"app"`
Title string `json:"title"`
Meta string `json:"meta"`
Initial string `json:"initial"`
RenderTime time.Duration `json:"-"`
}
// HTMLApp returns a application template
func (r Resp) HTMLApp() template.HTML {
return template.HTML(r.App)
}
// HTMLTitle returns a title data
func (r Resp) HTMLTitle() template.HTML {
return template.HTML(r.Title)
}
// HTMLMeta returns a meta data
func (r Resp) HTMLMeta() template.HTML {
return template.HTML(r.Meta)
}
// Interface to serve React app on demand or from prepared pool.
type pool interface {
get() *JSVM
put(*JSVM)
drop(*JSVM)
}
// newEnginePool return pool of JS vms.
func newEnginePool(filePath string, size int, proxy http.Handler) *enginePool {
fmt.Println("CREATING ENGINE POOL");
pool := &enginePool{
path: filePath,
ch: make(chan *JSVM, size),
proxy: proxy,
}
go func() {
for i := 0; i < size; i++ {
fmt.Println("ENGINE POOL");
pool.ch <- newJSVM(filePath, proxy)
}
}()
return pool
}
type enginePool struct {
ch chan *JSVM
path string
proxy http.Handler
}
func (o *enginePool) get() *JSVM {
fmt.Println("GET")
return <-o.ch
}
func (o *enginePool) put(ot *JSVM) {
fmt.Println("PUT")
o.ch <- ot
}
func (o *enginePool) drop(ot *JSVM) {
println("DROP");
ot.Stop()
ot = nil
o.ch <- newJSVM(o.path, o.proxy)
}
// newJSVM loads bundle.js into context.
func newJSVM(filePath string, proxy http.Handler) *JSVM {
fmt.Println("INIT JSVM")
vm := &JSVM{
EventLoop: eventloop.NewEventLoop(),
ch: make(chan Resp, 1),
}
vm.EventLoop.Start()
fetch.Enable(vm.EventLoop, proxy)
bundle, err := ioutil.ReadFile(filePath) // just pass the file name
if err != nil {
fmt.Print(err)
panic(err)
}
vm.EventLoop.RunOnLoop(func(_vm *goja.Runtime) {
fmt.Println("Running loop");
var seed int64
if err := binary.Read(crand.Reader, binary.LittleEndian, &seed); err != nil {
panic(fmt.Errorf("Could not read random bytes: %v", err))
}
_vm.SetRandSource(goja.RandSource(rand.New(rand.NewSource(seed)).Float64))
_, err := _vm.RunScript("bundle.js", string(bundle))
if err != nil {
if exception, ok := err.(*goja.Exception); ok {
panic(exception.String())
}
}
if fn, ok := goja.AssertFunction(_vm.Get("main")); ok {
vm.fn = fn
} else {
fmt.Println("fn assert failed")
return;
}
_vm.Set("__goServerCallback__", func(call goja.FunctionCall) goja.Value {
obj := call.Argument(0).Export().(map[string]interface{})
re := &Resp{}
for _, field := range structs.Fields(re) {
if n := field.Tag("json"); len(n) > 1 {
field.Set(obj[n])
}
}
vm.ch <- *re
return nil
})
})
return vm
}
// JSVM wraps goja EventLoop
type JSVM struct {
*eventloop.EventLoop
ch chan Resp
fn goja.Callable
}
// Handle handles http requests
func (r *JSVM) Handle(req map[string]interface{}) <-chan Resp {
r.EventLoop.RunOnLoop(func(vm *goja.Runtime) {
fmt.Println("RUN IN HANDLE")
v := vm.ToValue(req)
fmt.Println("REQ", req);
r.fn(nil, v, vm.ToValue("__goServerCallback__"))
})
return r.ch
}
type onDemandPool struct {
path string
proxy http.Handler
}
func (f *onDemandPool) get() *JSVM {
fmt.Println("GET JSVM");
return newJSVM(f.path, f.proxy)
}
func (f onDemandPool) put(c *JSVM) {
c.Stop()
}
func (f *onDemandPool) drop(c *JSVM) {
f.put(c)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment