Skip to content

Commit

Permalink
Merge pull request #94 from syumai/add-hono-middleware
Browse files Browse the repository at this point in the history
[experimental] add Hono middleware
  • Loading branch information
syumai committed Feb 11, 2024
2 parents 484c7be + 8ea4e2b commit a392725
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 2 deletions.
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

0 comments on commit a392725

Please sign in to comment.