diff --git a/exp/hono/context.go b/exp/hono/context.go new file mode 100644 index 0000000..f9ccdcf --- /dev/null +++ b/exp/hono/context.go @@ -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) +} diff --git a/exp/hono/middleware.go b/exp/hono/middleware.go new file mode 100644 index 0000000..76fc462 --- /dev/null +++ b/exp/hono/middleware.go @@ -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 {} +} 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/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) +} 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{