From 8cf026a01b8ad1f5482b6f6d556d0e9239553c5f Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 19:30:23 +0900 Subject: [PATCH 1/7] [experimental] add Hono middleware --- exp/hono/context.go | 63 ++++++++++++++++++++++++++++++ exp/hono/header.go | 63 ++++++++++++++++++++++++++++++ exp/hono/middleware.go | 78 +++++++++++++++++++++++++++++++++++++ exp/hono/middleware_test.go | 32 +++++++++++++++ internal/jsutil/stream.go | 13 ++++++- 5 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 exp/hono/context.go create mode 100644 exp/hono/header.go create mode 100644 exp/hono/middleware.go create mode 100644 exp/hono/middleware_test.go diff --git a/exp/hono/context.go b/exp/hono/context.go new file mode 100644 index 0000000..94c5096 --- /dev/null +++ b/exp/hono/context.go @@ -0,0 +1,63 @@ +package hono + +import ( + "context" + "io" + "net/http" + "sync" + "syscall/js" + + "github.com/syumai/workers/internal/jshttp" + "github.com/syumai/workers/internal/jsutil" + "github.com/syumai/workers/internal/runtimecontext" +) + +type Context struct { + ctxObj js.Value + reqFunc func() *http.Request +} + +func newContext(ctxObj js.Value) *Context { + return &Context{ + ctxObj: ctxObj, + reqFunc: sync.OnceValue(func() *http.Request { + reqObj := ctxObj.Get("req").Get("raw") + req, err := jshttp.ToRequest(reqObj) + if err != nil { + panic(err) + } + ctx := runtimecontext.New(context.Background(), reqObj, jsutil.RuntimeContext) + req = req.WithContext(ctx) + return req + }), + } +} + +func (c *Context) Request() *http.Request { + return c.reqFunc() +} + +func (c *Context) Header() Header { + return &header{ + headerObj: c.ctxObj.Get("req").Get("headers"), + } +} + +func (c *Context) SetStatus(statusCode int) { + c.ctxObj.Call("status", statusCode) +} + +func (c *Context) ResponseBody() io.ReadCloser { + return jsutil.ConvertReadableStreamToReadCloser(c.ctxObj.Get("res").Get("body")) +} + +func (c *Context) SetResponseBody(body io.ReadCloser) { + var res js.Value + if sr, ok := body.(jsutil.RawJSBodyGetter); ok { + res = jsutil.ResponseClass.New(sr, c.ctxObj.Get("res")) + } else { + bodyObj := jsutil.ConvertReaderToReadableStream(body) + res = jsutil.ResponseClass.New(bodyObj, c.ctxObj.Get("res")) + } + c.ctxObj.Set("res", res) +} diff --git a/exp/hono/header.go b/exp/hono/header.go new file mode 100644 index 0000000..ed99375 --- /dev/null +++ b/exp/hono/header.go @@ -0,0 +1,63 @@ +package hono + +import ( + "strings" + "syscall/js" +) + +type Header interface { + Add(key, value string) + Set(key, value string) + Get(key string) string + Values(key string) []string + Entries() []HeaderEntry + // Write(w io.Writer) // TODO: implement + // Clone() httpHeader // Not planned to be implemented +} + +type HeaderEntry struct { + Key string + Values []string +} + +type header struct { + headerObj js.Value +} + +var _ Header = (*header)(nil) + +func (h *header) Add(key, value string) { + h.headerObj.Call("append", key, value) +} + +func (h *header) Set(key, value string) { + h.headerObj.Call("set", key, value) +} + +func (h *header) Get(key string) string { + vs := h.Values(key) + if len(vs) == 0 { + return "" + } + return vs[0] +} + +func (h *header) Values(key string) []string { + values := h.headerObj.Call("get", key).String() + return strings.Split(values, ",") +} + +func (h *header) Entries() []HeaderEntry { + var entries []HeaderEntry + entriesObj := js.Global().Get("Object").Call("entries", h.headerObj) + for i := 0; i < entriesObj.Length(); i++ { + entryObj := entriesObj.Index(i) + key := entryObj.Index(0).String() + values := entryObj.Index(1).String() + entries[i] = HeaderEntry{ + Key: key, + Values: strings.Split(values, ","), + } + } + return entries +} diff --git a/exp/hono/middleware.go b/exp/hono/middleware.go new file mode 100644 index 0000000..c574c55 --- /dev/null +++ b/exp/hono/middleware.go @@ -0,0 +1,78 @@ +package hono + +import ( + "fmt" + "syscall/js" + + "github.com/syumai/workers/internal/jsutil" +) + +type Middleware func(c *Context, next func()) + +var middleware Middleware + +func ChainMiddlewares(middlewares ...Middleware) Middleware { + if len(middlewares) == 0 { + return nil + } + if len(middlewares) == 1 { + return middlewares[0] + } + return func(c *Context, next func()) { + for i := len(middlewares) - 1; i > 0; i-- { + i := i + f := next + next = func() { + middlewares[i](c, f) + } + } + middlewares[0](c, next) + } +} + +func init() { + runHonoMiddlewareCallback := js.FuncOf(func(_ js.Value, args []js.Value) any { + if len(args) > 2 { + panic(fmt.Errorf("too many args given to handleRequest: %d", len(args))) + } + reqObj := args[0] + nextFnObj := args[1] + var cb js.Func + cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any { + defer cb.Release() + resolve := pArgs[0] + go func() { + err := runHonoMiddleware(reqObj, nextFnObj) + if err != nil { + panic(err) + } + resolve.Invoke(js.Undefined()) + }() + return js.Undefined() + }) + return js.Undefined() + }) + jsutil.Binding.Set("runHonoMiddleware", runHonoMiddlewareCallback) +} + +func runHonoMiddleware(reqObj, nextFnObj js.Value) error { + if middleware == nil { + return fmt.Errorf("ServeMiddleware must be called before runHonoMiddleware.") + } + c := newContext(reqObj) + next := func() { + jsutil.AwaitPromise(nextFnObj.Invoke()) + } + middleware(c, next) + return nil +} + +//go:wasmimport workers ready +func ready() + +// ServeMiddleware sets the Task to be executed +func ServeMiddleware(middleware_ Middleware) { + middleware = middleware_ + ready() + select {} +} diff --git a/exp/hono/middleware_test.go b/exp/hono/middleware_test.go new file mode 100644 index 0000000..70a8572 --- /dev/null +++ b/exp/hono/middleware_test.go @@ -0,0 +1,32 @@ +package hono + +import "testing" + +func TestChainMiddlewares(t *testing.T) { + result := "" + middlewares := []Middleware{ + func(c *Context, next func()) { + result += "1" + next() + result += "1" + }, + func(c *Context, next func()) { + result += "2" + next() + result += "2" + }, + func(c *Context, next func()) { + result += "3" + next() + result += "3" + }, + } + m := ChainMiddlewares(middlewares...) + m(nil, func() { + result += "0" + }) + const want = "1230321" + if result != want { + t.Errorf("result: got %q, want %q", result, want) + } +} diff --git a/internal/jsutil/stream.go b/internal/jsutil/stream.go index 158744d..e0597fd 100644 --- a/internal/jsutil/stream.go +++ b/internal/jsutil/stream.go @@ -11,6 +11,10 @@ type RawJSBodyWriter interface { WriteRawJSBody(body js.Value) } +type RawJSBodyGetter interface { + GetRawJSBody() js.Value +} + // readableStreamToReadCloser implements io.Reader sourced from ReadableStreamDefaultReader. // - ReadableStreamDefaultReader: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader // - This implementation is based on: https://deno.land/std@0.139.0/streams/conversion.ts#L76 @@ -21,8 +25,9 @@ type readableStreamToReadCloser struct { } var ( - _ io.ReadCloser = (*readableStreamToReadCloser)(nil) - _ io.WriterTo = (*readableStreamToReadCloser)(nil) + _ io.ReadCloser = (*readableStreamToReadCloser)(nil) + _ io.WriterTo = (*readableStreamToReadCloser)(nil) + _ RawJSBodyGetter = (*readableStreamToReadCloser)(nil) ) // Read reads bytes from ReadableStreamDefaultReader. @@ -91,6 +96,10 @@ func (sr *readableStreamToReadCloser) WriteTo(w io.Writer) (n int64, err error) return io.Copy(w, &readerWrapper{sr}) } +func (sr *readableStreamToReadCloser) GetRawJSBody() js.Value { + return sr.stream +} + // ConvertReadableStreamToReadCloser converts ReadableStream to io.ReadCloser. func ConvertReadableStreamToReadCloser(stream js.Value) io.ReadCloser { return &readableStreamToReadCloser{ From d9dc0f148041658b4ff0c415401c43d72e6a7ee9 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 19:51:22 +0900 Subject: [PATCH 2/7] remove reqObj from runHonoMiddleware func parameters --- exp/hono/middleware.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/exp/hono/middleware.go b/exp/hono/middleware.go index c574c55..bfae6de 100644 --- a/exp/hono/middleware.go +++ b/exp/hono/middleware.go @@ -32,17 +32,16 @@ func ChainMiddlewares(middlewares ...Middleware) Middleware { func init() { runHonoMiddlewareCallback := js.FuncOf(func(_ js.Value, args []js.Value) any { - if len(args) > 2 { + if len(args) > 1 { panic(fmt.Errorf("too many args given to handleRequest: %d", len(args))) } - reqObj := args[0] - nextFnObj := args[1] + nextFnObj := args[0] var cb js.Func cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any { defer cb.Release() resolve := pArgs[0] go func() { - err := runHonoMiddleware(reqObj, nextFnObj) + err := runHonoMiddleware(nextFnObj) if err != nil { panic(err) } @@ -55,11 +54,11 @@ func init() { jsutil.Binding.Set("runHonoMiddleware", runHonoMiddlewareCallback) } -func runHonoMiddleware(reqObj, nextFnObj js.Value) error { +func runHonoMiddleware(nextFnObj js.Value) error { if middleware == nil { return fmt.Errorf("ServeMiddleware must be called before runHonoMiddleware.") } - c := newContext(reqObj) + c := newContext(jsutil.RuntimeContext) next := func() { jsutil.AwaitPromise(nextFnObj.Invoke()) } From 6f021d5ba95e28a86575faedecfb32bf8a568402 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 19:54:58 +0900 Subject: [PATCH 3/7] fixed to return Promise from runHonoMiddleware func --- exp/hono/middleware.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/hono/middleware.go b/exp/hono/middleware.go index bfae6de..d63ad36 100644 --- a/exp/hono/middleware.go +++ b/exp/hono/middleware.go @@ -49,7 +49,7 @@ func init() { }() return js.Undefined() }) - return js.Undefined() + return jsutil.NewPromise(cb) }) jsutil.Binding.Set("runHonoMiddleware", runHonoMiddlewareCallback) } From 4b95d7f98c8c4e6ef92ad5a04ad8b218d4e5b8c4 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 21:55:49 +0900 Subject: [PATCH 4/7] fix bugs of hono middleware support --- exp/hono/context.go | 18 ++++++++---------- exp/hono/middleware.go | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/exp/hono/context.go b/exp/hono/context.go index 94c5096..0b9aff3 100644 --- a/exp/hono/context.go +++ b/exp/hono/context.go @@ -37,10 +37,8 @@ func (c *Context) Request() *http.Request { return c.reqFunc() } -func (c *Context) Header() Header { - return &header{ - headerObj: c.ctxObj.Get("req").Get("headers"), - } +func (c *Context) SetHeader(key, value string) { + c.ctxObj.Call("header", key, value) } func (c *Context) SetStatus(statusCode int) { @@ -51,13 +49,13 @@ func (c *Context) ResponseBody() io.ReadCloser { return jsutil.ConvertReadableStreamToReadCloser(c.ctxObj.Get("res").Get("body")) } -func (c *Context) SetResponseBody(body io.ReadCloser) { - var res js.Value +func (c *Context) SetBody(body io.ReadCloser) { + var bodyObj js.Value if sr, ok := body.(jsutil.RawJSBodyGetter); ok { - res = jsutil.ResponseClass.New(sr, c.ctxObj.Get("res")) + bodyObj = sr.GetRawJSBody() } else { - bodyObj := jsutil.ConvertReaderToReadableStream(body) - res = jsutil.ResponseClass.New(bodyObj, c.ctxObj.Get("res")) + bodyObj = jsutil.ConvertReaderToReadableStream(body) } - c.ctxObj.Set("res", res) + respObj := c.ctxObj.Call("body", bodyObj) + c.ctxObj.Set("res", respObj) } diff --git a/exp/hono/middleware.go b/exp/hono/middleware.go index d63ad36..76fc462 100644 --- a/exp/hono/middleware.go +++ b/exp/hono/middleware.go @@ -58,7 +58,7 @@ func runHonoMiddleware(nextFnObj js.Value) error { if middleware == nil { return fmt.Errorf("ServeMiddleware must be called before runHonoMiddleware.") } - c := newContext(jsutil.RuntimeContext) + c := newContext(jsutil.RuntimeContext.Get("ctx")) next := func() { jsutil.AwaitPromise(nextFnObj.Invoke()) } From bd88dd615159e69bad8997bb8934c54d042b4172 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 21:56:32 +0900 Subject: [PATCH 5/7] remove unused Header impl of hono middleware support --- exp/hono/header.go | 63 ---------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 exp/hono/header.go diff --git a/exp/hono/header.go b/exp/hono/header.go deleted file mode 100644 index ed99375..0000000 --- a/exp/hono/header.go +++ /dev/null @@ -1,63 +0,0 @@ -package hono - -import ( - "strings" - "syscall/js" -) - -type Header interface { - Add(key, value string) - Set(key, value string) - Get(key string) string - Values(key string) []string - Entries() []HeaderEntry - // Write(w io.Writer) // TODO: implement - // Clone() httpHeader // Not planned to be implemented -} - -type HeaderEntry struct { - Key string - Values []string -} - -type header struct { - headerObj js.Value -} - -var _ Header = (*header)(nil) - -func (h *header) Add(key, value string) { - h.headerObj.Call("append", key, value) -} - -func (h *header) Set(key, value string) { - h.headerObj.Call("set", key, value) -} - -func (h *header) Get(key string) string { - vs := h.Values(key) - if len(vs) == 0 { - return "" - } - return vs[0] -} - -func (h *header) Values(key string) []string { - values := h.headerObj.Call("get", key).String() - return strings.Split(values, ",") -} - -func (h *header) Entries() []HeaderEntry { - var entries []HeaderEntry - entriesObj := js.Global().Get("Object").Call("entries", h.headerObj) - for i := 0; i < entriesObj.Length(); i++ { - entryObj := entriesObj.Index(i) - key := entryObj.Index(0).String() - values := entryObj.Index(1).String() - entries[i] = HeaderEntry{ - Key: key, - Values: strings.Split(values, ","), - } - } - return entries -} From fa22eea00b56b354bb1b7e530e41b1604a16df38 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 23:12:28 +0900 Subject: [PATCH 6/7] split SetResponse func of Hono --- exp/hono/context.go | 11 +++++------ exp/hono/response.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 exp/hono/response.go diff --git a/exp/hono/context.go b/exp/hono/context.go index 0b9aff3..8f78687 100644 --- a/exp/hono/context.go +++ b/exp/hono/context.go @@ -50,12 +50,11 @@ func (c *Context) ResponseBody() io.ReadCloser { } func (c *Context) SetBody(body io.ReadCloser) { - var bodyObj js.Value - if sr, ok := body.(jsutil.RawJSBodyGetter); ok { - bodyObj = sr.GetRawJSBody() - } else { - bodyObj = jsutil.ConvertReaderToReadableStream(body) - } + bodyObj := convertBodyToJS(body) respObj := c.ctxObj.Call("body", bodyObj) c.ctxObj.Set("res", respObj) } + +func (c *Context) SetResponse(respObj js.Value) { + c.ctxObj.Set("res", respObj) +} diff --git a/exp/hono/response.go b/exp/hono/response.go new file mode 100644 index 0000000..3089998 --- /dev/null +++ b/exp/hono/response.go @@ -0,0 +1,35 @@ +package hono + +import ( + "io" + "net/http" + "syscall/js" + + "github.com/syumai/workers/internal/jshttp" + "github.com/syumai/workers/internal/jsutil" +) + +func convertBodyToJS(body io.ReadCloser) js.Value { + if sr, ok := body.(jsutil.RawJSBodyGetter); ok { + return sr.GetRawJSBody() + } + return jsutil.ConvertReaderToReadableStream(body) +} + +func NewJSResponse(body io.ReadCloser, statusCode int, headers http.Header) js.Value { + bodyObj := convertBodyToJS(body) + opts := jsutil.ObjectClass.New() + if statusCode != 0 { + opts.Set("status", statusCode) + } + if headers != nil { + headersObj := jshttp.ToJSHeader(headers) + opts.Set("headers", headersObj) + } + return jsutil.ResponseClass.New(bodyObj, opts) +} + +func NewJSResponseWithBase(body io.ReadCloser, baseRespObj js.Value) js.Value { + bodyObj := convertBodyToJS(body) + return jsutil.ResponseClass.New(bodyObj, baseRespObj) +} From 8ea4e2bf5f5636f4764b11c6ed96700df7b2dbf8 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 11 Feb 2024 23:16:11 +0900 Subject: [PATCH 7/7] add RawResponse getter to hono context --- exp/hono/context.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exp/hono/context.go b/exp/hono/context.go index 8f78687..f9ccdcf 100644 --- a/exp/hono/context.go +++ b/exp/hono/context.go @@ -45,6 +45,10 @@ func (c *Context) SetStatus(statusCode int) { c.ctxObj.Call("status", statusCode) } +func (c *Context) RawResponse() js.Value { + return c.ctxObj.Get("res") +} + func (c *Context) ResponseBody() io.ReadCloser { return jsutil.ConvertReadableStreamToReadCloser(c.ctxObj.Get("res").Get("body")) }