diff --git a/README.md b/README.md index 8a0d4e1..468a8ac 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Hello example with state and keepalive](https://nlepage.github.io/go-wasm-http-server/hello-state-keepalive) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state-keepalive)) - [😺 Catption generator example](https://nlepage.github.io/catption/wasm) ([sources](https://github.com/nlepage/catption/tree/wasm)) - [Random password generator web server](https://nlepage.github.io/random-password-please/) ([sources](https://github.com/nlepage/random-password-please) forked from [jbarham/random-password-please](https://github.com/jbarham/random-password-please)) + - [Server fallbacks, and compiling with TinyGo](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/tinygo) (runs locally; see [sources & readme](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/tinygo#README) for how to run this example) ## How? @@ -39,6 +40,7 @@ The slides are available [here](https://nlepage.github.io/go-wasm-http-talk/). `go-wasm-http-server` requires you to build your Go application to WebAssembly, so you need to make sure your code is compatible: - no C bindings - no System dependencies such as file system or network (database server for example) +- For smaller WASM blobs, your code may also benefit from being compatible with, and compiled by, [TinyGo](https://tinygo.org/docs/reference/lang-support/stdlib/). See the TinyGo specific details below. ## Usage @@ -87,16 +89,36 @@ You may want to use build tags as shown above (or file name suffixes) in order t Then build your WebAssembly binary: ```sh +# To compile with Go GOOS=js GOARCH=wasm go build -o server.wasm . + +# To compile with TinyGo, if your code is compatible +GOOS=js GOARCH=wasm tinygo build -o server.wasm . ``` ### Step 2: Create ServiceWorker file +First, check the version of Go/TinyGo you compiled your wasm with: + +```sh +$ go version +go version go1.23.4 darwin/arm64 +# ^------^ + +$ tinygo version +tinygo version 0.35.0 darwin/arm64 (using go version go1.23.4 and LLVM version 18.1.2) +# ^----^ +``` + Create a ServiceWorker file with the following code: 📄 `sw.js` ```js -importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.18.4/misc/wasm/wasm_exec.js') +// Note the 'go.1.23.4' below, that matches the version you just found: +importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.4/misc/wasm/wasm_exec.js') +// If you compiled with TinyGo then, similarly, use: +importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js') + importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.5/sw.js') registerWasmHTTPListener('path/to/server.wasm') diff --git a/docs/tinygo/README.md b/docs/tinygo/README.md new file mode 100644 index 0000000..8bf9a87 --- /dev/null +++ b/docs/tinygo/README.md @@ -0,0 +1,42 @@ +# Compiling with TinyGo + +This example demonstrates that go-wasm-http-server can also be compiled with [TinyGo](https://www.tinygo.org), producing significantly smaller WASM blobs, though at the expense of [at least one known bug](https://github.com/tinygo-org/tinygo/issues/1140) and a [reduced standard library](https://tinygo.org/docs/reference/lang-support/stdlib/). + +This example also demonstrates how the same code can be used for both server-side execution, and client-side execution in WASM (providing support for clients that cannot interpret WASM). + +## Prerequisites + +You'll need a version of [TinyGo installed](https://tinygo.org/getting-started/install/). (eg. `brew install tinygo-org/tools/tinygo`) + +You'll need to make sure the first line of `sw.js` here has the same tinygo version number as your TinyGo version (this was v0.35.0 at time of writing). + +## Build & run + +Compile the WASM blob with TinyGo (this has been done for you for this example): + +```bash +GOOS=js GOARCH=wasm tinygo build -o api.wasm . +``` + +Run the server (with Go, not TinyGo): + +```bash +$ go run . +Server starting on http://127.0.0.1: +``` + +## Important notes + +You **must** use the TinyGo `wasm_exec.js`, specific to the version of TinyGo used to compile the WASM, in your `sw.js`. For example, if using the JSDelivr CDN: + +```js +importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js') +``` + +Note that the `0.35.0` within the path matches the TinyGo version used: + +```sh +$ tinygo version +tinygo version 0.35.0 darwin/arm64 (using go version go1.23.4 and LLVM version 18.1.2) +# ^----^ +``` diff --git a/docs/tinygo/api.wasm b/docs/tinygo/api.wasm new file mode 100644 index 0000000..2730abb Binary files /dev/null and b/docs/tinygo/api.wasm differ diff --git a/docs/tinygo/handlers.go b/docs/tinygo/handlers.go new file mode 100644 index 0000000..09a92d5 --- /dev/null +++ b/docs/tinygo/handlers.go @@ -0,0 +1,19 @@ +package main + +import ( + "encoding/json" + "net/http" + "runtime" +) + +func goRuntimeHandler(res http.ResponseWriter, req *http.Request) { + res.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(res).Encode(map[string]string{ + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "compiler": runtime.Compiler, + "version": runtime.Version(), + }); err != nil { + panic(err) + } +} diff --git a/docs/tinygo/index.html b/docs/tinygo/index.html new file mode 100644 index 0000000..d6f24ca --- /dev/null +++ b/docs/tinygo/index.html @@ -0,0 +1,44 @@ + + + + go-wasm-http-server tinygo demo + + + +

This example demonstrates that go-wasm-http-server can be compiled with TinyGo, producing significantly smaller WASM blobs, at the expense of at least one known bug, and a reduced standard library.

+
WASM HTTP Service Worker:
☁️ Not loaded — will call server
+ +
    +
  1. +
  2. +
  3. Call the API again (Step 1)
  4. +
+ +

Response:

+

+  
+
diff --git a/docs/tinygo/server.go b/docs/tinygo/server.go
new file mode 100644
index 0000000..d9a5bbe
--- /dev/null
+++ b/docs/tinygo/server.go
@@ -0,0 +1,34 @@
+//go:build !wasm
+// +build !wasm
+
+package main
+
+import (
+	"embed"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+)
+
+//go:embed *.html *.js *.wasm
+var thisDir embed.FS
+
+func main() {
+	// Serve all files in this directory statically
+	http.Handle("/", http.FileServer(http.FS(thisDir)))
+
+	// Note that this needs to be mounted at /api/tiny, rather than just /tiny (like in wasm.go)
+	// because the service worker mounts the WASM server at /api (at the end of sw.js)
+	http.HandleFunc("/api/tiny", goRuntimeHandler)
+
+	// Pick any available port. Note that ServiceWorkers _require_ localhost for non-SSL serving (so other LAN/WAN IPs will prevent the service worker from loading)
+	listener, err := net.Listen("tcp", ":0")
+	if err != nil {
+		log.Fatalf("Unable to claim a port to start server on: %v", err)
+	}
+
+	// Share the port being used & start
+	fmt.Printf("Server starting on http://127.0.0.1:%d\n", listener.Addr().(*net.TCPAddr).Port)
+	panic(http.Serve(listener, nil))
+}
diff --git a/docs/tinygo/sw.js b/docs/tinygo/sw.js
new file mode 100644
index 0000000..9e351e3
--- /dev/null
+++ b/docs/tinygo/sw.js
@@ -0,0 +1,14 @@
+importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js')
+importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.5/sw.js')
+
+const wasm = 'api.wasm'
+
+addEventListener('install', (event) => {
+  event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)))
+})
+
+addEventListener('activate', (event) => {
+  event.waitUntil(clients.claim())
+})
+
+registerWasmHTTPListener(wasm, { base: 'api' })
diff --git a/docs/tinygo/wasm.go b/docs/tinygo/wasm.go
new file mode 100644
index 0000000..f55bad5
--- /dev/null
+++ b/docs/tinygo/wasm.go
@@ -0,0 +1,18 @@
+//go:build wasm
+// +build wasm
+
+package main
+
+import (
+	"net/http"
+
+	wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
+)
+
+func main() {
+	http.HandleFunc("/tiny", goRuntimeHandler)
+
+	wasmhttp.Serve(nil)
+
+	select {}
+}
diff --git a/go.sum b/go.sum
index b491b36..27a9218 100644
--- a/go.sum
+++ b/go.sum
@@ -4,3 +4,6 @@ github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182
 github.com/nlepage/go-js-promise v1.0.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo=
 github.com/tmaxmax/go-sse v0.8.0 h1:pPpTgyyi1r7vG2o6icebnpGEh3ebcnBXqDWkb7aTofs=
 github.com/tmaxmax/go-sse v0.8.0/go.mod h1:HLoxqxdH+7oSUItjtnpxjzJedfr/+Rrm/dNWBcTxJFM=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
diff --git a/internal/readablestream/reader.go b/internal/readablestream/reader.go
index 854f6fd..1842d4d 100644
--- a/internal/readablestream/reader.go
+++ b/internal/readablestream/reader.go
@@ -14,7 +14,7 @@ type Reader struct {
 	off   int
 }
 
-var _ io.Reader = (*Reader)(nil)
+var _ io.ReadCloser = (*Reader)(nil)
 
 func NewReader(r safejs.Value) *Reader {
 	return &Reader{
@@ -83,3 +83,14 @@ func (r *Reader) Read(p []byte) (int, error) {
 
 	return n, nil
 }
+
+func (r *Reader) Close() error {
+	p, err := r.value.Call("cancel")
+	if err != nil {
+		return err
+	}
+
+	_, err = promise.Await(safejs.Unsafe(p))
+
+	return err
+}
diff --git a/request.go b/request.go
index 5f414f6..63de281 100644
--- a/request.go
+++ b/request.go
@@ -3,7 +3,7 @@ package wasmhttp
 import (
 	"io"
 	"net/http"
-	"net/http/httptest"
+	"net/url"
 	"syscall/js"
 
 	promise "github.com/nlepage/go-js-promise"
@@ -20,7 +20,11 @@ func Request(uvalue js.Value) (*http.Request, error) {
 		return nil, err
 	}
 
-	url, err := value.GetString("url")
+	rawURL, err := value.GetString("url")
+	if err != nil {
+		return nil, err
+	}
+	u, err := url.Parse(rawURL)
 	if err != nil {
 		return nil, err
 	}
@@ -30,7 +34,7 @@ func Request(uvalue js.Value) (*http.Request, error) {
 		return nil, err
 	}
 
-	var bodyReader io.Reader
+	var bodyReader io.ReadCloser
 
 	if !body.IsNull() {
 		// WORKAROUND: Firefox does not have request.body ReadableStream
@@ -59,11 +63,15 @@ func Request(uvalue js.Value) (*http.Request, error) {
 		bodyReader = readablestream.NewReader(r)
 	}
 
-	req := httptest.NewRequest(
-		method,
-		url,
-		bodyReader,
-	)
+	req := &http.Request{
+		Method:     method,
+		URL:        u,
+		Body:       bodyReader,
+		Header:     make(http.Header),
+		Proto:      "HTTP/1.1",
+		ProtoMajor: 1,
+		ProtoMinor: 1,
+	}
 
 	headers, err := value.Get("headers")
 	if err != nil {