支持 await/async
This commit is contained in:
5
examples/event/.pages.yaml
Normal file
5
examples/event/.pages.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
routes:
|
||||||
|
- path: "**"
|
||||||
|
js:
|
||||||
|
exec: "index.js"
|
||||||
|
debug: true
|
||||||
12
examples/event/index.html
Normal file
12
examples/event/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>JS 概念验证</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
examples/event/index.js
Normal file
18
examples/event/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
console.log('boot');
|
||||||
|
(async () => {
|
||||||
|
console.log(0);
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(1000);
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(2000);
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(3000);
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(4000);
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(5000);
|
||||||
|
})();
|
||||||
|
console.log('boot end');
|
||||||
@@ -72,7 +72,7 @@ func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string,
|
|||||||
result := &PageContent{}
|
result := &PageContent{}
|
||||||
meta, err := p.GetMeta(ctx, owner, repo, branch)
|
meta, err := p.GetMeta(ctx, owner, repo, branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Debug("查询错误", zap.Error(err))
|
zap.L().Debug("repo does not exists", zap.Error(err), zap.Strings("meta", []string{owner, repo, branch}))
|
||||||
if meta != nil {
|
if meta != nil {
|
||||||
// 解析错误汇报
|
// 解析错误汇报
|
||||||
return nil, errors.New(meta.ErrorMsg)
|
return nil, errors.New(meta.ErrorMsg)
|
||||||
@@ -85,7 +85,7 @@ func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string,
|
|||||||
result.Path = strings.Join(path, "/")
|
result.Path = strings.Join(path, "/")
|
||||||
|
|
||||||
if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil {
|
if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil {
|
||||||
zap.L().Warn("别名绑定失败", zap.Error(err))
|
zap.L().Warn("alias binding error.", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<style>
|
<style>
|
||||||
|
.clear{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -70,7 +74,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-header">控制台日志</div>
|
<div class="section-header">控制台日志</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<div class="log">{{- range .Logs }}<p>{{ .Time.Format "2006/01/02 15:04:05" }} - {{ .Level }} : {{ .Message }}</p>{{- end }}</div>
|
<div class="log">{{- range .Logs }}<p class="clear">{{ .Time.Format "2006/01/02 15:04:05" }} - {{ .Level }} : {{ .Message }}</p>{{- end }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package goja
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/console"
|
"github.com/dop251/goja_nodejs/console"
|
||||||
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
"github.com/dop251/goja_nodejs/require"
|
"github.com/dop251/goja_nodejs/require"
|
||||||
"github.com/dop251/goja_nodejs/url"
|
"github.com/dop251/goja_nodejs/url"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gopkg.d7z.net/gitea-pages/pkg/core"
|
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,40 +24,56 @@ var FilterInstGoJa core.FilterInstance = func(config core.FilterParams) (core.Fi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if param.Exec == "" {
|
if param.Exec == "" {
|
||||||
return nil, errors.Errorf("no exec specified")
|
return nil, errors.New("no exec specified")
|
||||||
}
|
}
|
||||||
return func(ctx core.FilterContext, w http.ResponseWriter, request *http.Request, next core.NextCall) error {
|
return func(ctx core.FilterContext, w http.ResponseWriter, request *http.Request, next core.NextCall) error {
|
||||||
js, err := ctx.ReadString(ctx, param.Exec)
|
js, err := ctx.ReadString(ctx, param.Exec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
prg, err := goja.Compile("main.js", js, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
debug := NewDebug(param.Debug && request.URL.Query().Get("debug") == "true", request, w)
|
debug := NewDebug(param.Debug && request.URL.Query().Get("debug") == "true", request, w)
|
||||||
registry := newRegistry(ctx)
|
registry := newRegistry(ctx)
|
||||||
registry.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(debug))
|
registry.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(debug))
|
||||||
vm := goja.New()
|
loop := eventloop.NewEventLoop(eventloop.WithRegistry(registry), eventloop.EnableConsole(true))
|
||||||
_ = registry.Enable(vm)
|
stop := make(chan struct{}, 1)
|
||||||
console.Enable(vm)
|
shutdown := make(chan struct{}, 1)
|
||||||
url.Enable(vm)
|
timeout, cancelFunc := context.WithTimeout(ctx, 3*time.Second)
|
||||||
if err = RequestInject(ctx, vm, request); err != nil {
|
defer cancelFunc()
|
||||||
return err
|
count := 0
|
||||||
}
|
|
||||||
if err = ResponseInject(vm, debug, request); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = KVInject(ctx, vm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
coreCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
shutdown <- struct{}{}
|
||||||
|
close(shutdown)
|
||||||
|
}()
|
||||||
select {
|
select {
|
||||||
case <-coreCtx.Done():
|
case <-timeout.Done():
|
||||||
vm.Interrupt(coreCtx.Err())
|
case <-stop:
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
count = loop.Stop()
|
||||||
}()
|
}()
|
||||||
_, err = vm.RunScript(param.Exec, js)
|
loop.Run(func(vm *goja.Runtime) {
|
||||||
cancel()
|
url.Enable(vm)
|
||||||
|
if err = RequestInject(ctx, vm, request); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = ResponseInject(vm, debug, request); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = KVInject(ctx, vm); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = vm.RunProgram(prg)
|
||||||
|
})
|
||||||
|
stop <- struct{}{}
|
||||||
|
close(stop)
|
||||||
|
<-shutdown
|
||||||
|
if count != 0 {
|
||||||
|
err = errors.Join(context.DeadlineExceeded, err)
|
||||||
|
}
|
||||||
return debug.Flush(err)
|
return debug.Flush(err)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runt
|
|||||||
}
|
}
|
||||||
return jsCtx.ToValue(get)
|
return jsCtx.ToValue(get)
|
||||||
},
|
},
|
||||||
"set": func(key string, value string) {
|
"set": func(key, value string) {
|
||||||
err := db.Put(ctx, key, value, kv.TTLKeep)
|
err := db.Put(ctx, key, value, kv.TTLKeep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -52,6 +52,20 @@ func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runt
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
},
|
},
|
||||||
|
"putIfNotExists": func(key, value string) bool {
|
||||||
|
exists, err := db.PutIfNotExists(ctx, key, value, kv.TTLKeep)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return exists
|
||||||
|
},
|
||||||
|
"compareAndSwap": func(key, oldValue, newValue string) bool {
|
||||||
|
swap, err := db.CompareAndSwap(ctx, key, oldValue, newValue)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return swap
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.Request) error {
|
func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.Request) error {
|
||||||
return jsCtx.GlobalObject().Set("response", map[string]any{
|
return jsCtx.GlobalObject().Set("response", map[string]any{
|
||||||
// 响应头操作
|
// 响应头操作
|
||||||
"setHeader": func(key string, value string) {
|
"setHeader": func(key, value string) {
|
||||||
writer.Header().Set(key, value)
|
writer.Header().Set(key, value)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 设置 cookie
|
// 设置 cookie
|
||||||
"setCookie": func(name string, value string, options ...map[string]interface{}) {
|
"setCookie": func(name, value string, options ...map[string]interface{}) {
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.d7z.net/gitea-pages/pkg/core"
|
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||||
"gopkg.d7z.net/gitea-pages/pkg/filters"
|
"gopkg.d7z.net/gitea-pages/pkg/filters"
|
||||||
|
"gopkg.d7z.net/gitea-pages/pkg/utils"
|
||||||
"gopkg.d7z.net/middleware/cache"
|
"gopkg.d7z.net/middleware/cache"
|
||||||
"gopkg.d7z.net/middleware/kv"
|
"gopkg.d7z.net/middleware/kv"
|
||||||
"gopkg.d7z.net/middleware/tools"
|
"gopkg.d7z.net/middleware/tools"
|
||||||
@@ -61,29 +62,32 @@ func NewPageServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, request *http.Request) {
|
||||||
sessionID, _ := uuid.NewRandom()
|
sessionID, _ := uuid.NewRandom()
|
||||||
request.Header.Set("Session-ID", sessionID.String())
|
request.Header.Set("Session-ID", sessionID.String())
|
||||||
//if s.staticFS != nil && strings.HasPrefix(request.URL.Path, staticPrefix) {
|
writer := utils.NewWrittenResponseWriter(w)
|
||||||
// s.staticFS.ServeHTTP(writer, request)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if e := recover(); e != nil {
|
if e := recover(); e != nil {
|
||||||
zap.L().Error("panic!", zap.Any("error", e), zap.Any("id", sessionID))
|
zap.L().Error("panic!", zap.Any("error", e), zap.Any("id", sessionID))
|
||||||
if err, ok := e.(error); ok {
|
if !writer.IsWritten() {
|
||||||
s.errorHandler(writer, request, err)
|
if err, ok := e.(error); ok {
|
||||||
|
s.errorHandler(writer, request, err)
|
||||||
|
} else {
|
||||||
|
s.errorHandler(writer, request, errors.New("panic"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err := s.Serve(writer, request)
|
err := s.Serve(writer, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Debug("错误请求", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionID))
|
zap.L().Debug("bad request.", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionID))
|
||||||
s.errorHandler(writer, request, err)
|
if !writer.IsWritten() {
|
||||||
|
s.errorHandler(writer, request, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error {
|
func (s *Server) Serve(writer *utils.WrittenResponseWriter, request *http.Request) error {
|
||||||
ctx := request.Context()
|
ctx := request.Context()
|
||||||
domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "")
|
domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "")
|
||||||
meta, err := s.meta.ParseDomainMeta(ctx, domain, request.URL.Path, request.URL.Query().Get("branch"))
|
meta, err := s.meta.ParseDomainMeta(ctx, domain, request.URL.Path, request.URL.Query().Get("branch"))
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
func ClearDuplicates[T comparable](slice []T) []T {
|
|
||||||
seen := make(map[T]bool)
|
|
||||||
for _, val := range slice {
|
|
||||||
seen[val] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []T
|
|
||||||
for key := range seen {
|
|
||||||
result = append(result, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
33
pkg/utils/resp.go
Normal file
33
pkg/utils/resp.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type WrittenResponseWriter struct {
|
||||||
|
write bool
|
||||||
|
base http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWrittenResponseWriter(base http.ResponseWriter) *WrittenResponseWriter {
|
||||||
|
return &WrittenResponseWriter{
|
||||||
|
base: base,
|
||||||
|
write: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrittenResponseWriter) Header() http.Header {
|
||||||
|
return w.base.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrittenResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
w.write = true
|
||||||
|
return w.base.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrittenResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.write = true
|
||||||
|
w.base.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrittenResponseWriter) IsWritten() bool {
|
||||||
|
return w.write
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -62,8 +61,34 @@ routes:
|
|||||||
assert.Equal(t, "POST /api/v1/fetch", string(data))
|
assert.Equal(t, "POST /api/v1/fetch", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_GoJa_Async(t *testing.T) {
|
||||||
|
server := core.NewDefaultTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
|
server.AddFile("org1/repo1/gh-pages/index.js", `
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
(async()=>{
|
||||||
|
await sleep(1000)
|
||||||
|
response.write('abc')
|
||||||
|
})()
|
||||||
|
|
||||||
|
`)
|
||||||
|
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
||||||
|
routes:
|
||||||
|
- path: "**"
|
||||||
|
js:
|
||||||
|
exec: "index.js"
|
||||||
|
`)
|
||||||
|
|
||||||
|
data, _, err := server.OpenFile("https://org1.example.com/repo1/api/v1/fetch")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "abc", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
func Benchmark_GoJa_Request(b *testing.B) {
|
func Benchmark_GoJa_Request(b *testing.B) {
|
||||||
_ = os.Setenv("BM", "1")
|
b.Setenv("BM", "1")
|
||||||
server := core.NewDefaultTestServer()
|
server := core.NewDefaultTestServer()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
|
|||||||
Reference in New Issue
Block a user