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{