支持 await/async

This commit is contained in:
ExplodingDragon
2025-11-18 00:22:11 +08:00
parent e1091fcf22
commit 562413b3bf
17 changed files with 172 additions and 55 deletions

View File

@@ -0,0 +1,5 @@
routes:
- path: "**"
js:
exec: "index.js"
debug: true

12
examples/event/index.html Normal file
View 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
View 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');

View File

@@ -72,7 +72,7 @@ func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string,
result := &PageContent{}
meta, err := p.GetMeta(ctx, owner, repo, branch)
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 {
// 解析错误汇报
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, "/")
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 result, nil

View File

@@ -7,6 +7,10 @@
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">
<style>
.clear{
padding: 0;
margin: 0;
}
body {
font-family: Arial, sans-serif;
margin: 0;
@@ -70,7 +74,7 @@
<div class="section">
<div class="section-header">控制台日志</div>
<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>

View File

@@ -2,15 +2,16 @@ package goja
import (
"context"
"errors"
"net/http"
"path/filepath"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/require"
"github.com/dop251/goja_nodejs/url"
"github.com/pkg/errors"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
@@ -23,40 +24,56 @@ var FilterInstGoJa core.FilterInstance = func(config core.FilterParams) (core.Fi
return nil, err
}
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 {
js, err := ctx.ReadString(ctx, param.Exec)
if err != nil {
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)
registry := newRegistry(ctx)
registry.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(debug))
vm := goja.New()
_ = registry.Enable(vm)
console.Enable(vm)
url.Enable(vm)
if err = RequestInject(ctx, vm, request); err != nil {
return err
}
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()
loop := eventloop.NewEventLoop(eventloop.WithRegistry(registry), eventloop.EnableConsole(true))
stop := make(chan struct{}, 1)
shutdown := make(chan struct{}, 1)
timeout, cancelFunc := context.WithTimeout(ctx, 3*time.Second)
defer cancelFunc()
count := 0
go func() {
defer func() {
shutdown <- struct{}{}
close(shutdown)
}()
select {
case <-coreCtx.Done():
vm.Interrupt(coreCtx.Err())
return
case <-timeout.Done():
case <-stop:
}
count = loop.Stop()
}()
_, err = vm.RunScript(param.Exec, js)
cancel()
loop.Run(func(vm *goja.Runtime) {
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)
}, nil
}

View File

@@ -39,7 +39,7 @@ func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runt
}
return jsCtx.ToValue(get)
},
"set": func(key string, value string) {
"set": func(key, value string) {
err := db.Put(ctx, key, value, kv.TTLKeep)
if err != nil {
panic(err)
@@ -52,6 +52,20 @@ func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runt
}
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
},
})
}
}

View File

@@ -11,7 +11,7 @@ import (
func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.Request) error {
return jsCtx.GlobalObject().Set("response", map[string]any{
// 响应头操作
"setHeader": func(key string, value string) {
"setHeader": func(key, value string) {
writer.Header().Set(key, value)
},
@@ -97,7 +97,7 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R
},
// 设置 cookie
"setCookie": func(name string, value string, options ...map[string]interface{}) {
"setCookie": func(name, value string, options ...map[string]interface{}) {
cookie := &http.Cookie{
Name: name,
Value: value,

View File

@@ -15,6 +15,7 @@ import (
"go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg/core"
"gopkg.d7z.net/gitea-pages/pkg/filters"
"gopkg.d7z.net/gitea-pages/pkg/utils"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv"
"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()
request.Header.Set("Session-ID", sessionID.String())
//if s.staticFS != nil && strings.HasPrefix(request.URL.Path, staticPrefix) {
// s.staticFS.ServeHTTP(writer, request)
// return
//}
writer := utils.NewWrittenResponseWriter(w)
defer func() {
if e := recover(); e != nil {
zap.L().Error("panic!", zap.Any("error", e), zap.Any("id", sessionID))
if err, ok := e.(error); ok {
s.errorHandler(writer, request, err)
if !writer.IsWritten() {
if err, ok := e.(error); ok {
s.errorHandler(writer, request, err)
} else {
s.errorHandler(writer, request, errors.New("panic"))
}
}
}
}()
err := s.Serve(writer, request)
if err != nil {
zap.L().Debug("错误请求", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionID))
s.errorHandler(writer, request, err)
zap.L().Debug("bad request.", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionID))
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()
domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "")
meta, err := s.meta.ParseDomainMeta(ctx, domain, request.URL.Path, request.URL.Query().Get("branch"))

View File

@@ -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
View 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
}

View File

@@ -2,7 +2,6 @@ package tests
import (
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
@@ -62,8 +61,34 @@ routes:
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) {
_ = os.Setenv("BM", "1")
b.Setenv("BM", "1")
server := core.NewDefaultTestServer()
defer server.Close()
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")