Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experimental] add Hono middleware #94

Merged
merged 7 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions exp/hono/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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) SetHeader(key, value string) {
c.ctxObj.Call("header", key, value)
}

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

func (c *Context) SetBody(body io.ReadCloser) {
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)
}
77 changes: 77 additions & 0 deletions exp/hono/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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) > 1 {
panic(fmt.Errorf("too many args given to handleRequest: %d", len(args)))
}
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(nextFnObj)
if err != nil {
panic(err)
}
resolve.Invoke(js.Undefined())
}()
return js.Undefined()
})
return jsutil.NewPromise(cb)
})
jsutil.Binding.Set("runHonoMiddleware", runHonoMiddlewareCallback)
}

func runHonoMiddleware(nextFnObj js.Value) error {
if middleware == nil {
return fmt.Errorf("ServeMiddleware must be called before runHonoMiddleware.")
}
c := newContext(jsutil.RuntimeContext.Get("ctx"))
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 {}
}
32 changes: 32 additions & 0 deletions exp/hono/middleware_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
35 changes: 35 additions & 0 deletions exp/hono/response.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 11 additions & 2 deletions internal/jsutil/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]/streams/conversion.ts#L76
Expand All @@ -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.
Expand Down Expand Up @@ -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{
Expand Down
Loading