支持 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{} 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

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"> 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>

View File

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

View File

@@ -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
},
}) })
} }
} }

View File

@@ -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,

View File

@@ -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"))

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 ( 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")