27 Commits

Author SHA1 Message Date
Nicolas Lepage
c94dcd965d chore: v2.0.4 2024-11-27 22:33:50 +01:00
Nicolas Lepage
2d786bdb14 fix: listen for ReadableStream cancellation 2024-11-27 22:32:19 +01:00
Nicolas Lepage
c561826125 chore: v2.0.3 2024-10-17 01:12:34 +02:00
Nicolas Lepage
897626b7d1 fix: Firefox does not have request.body ReadableStream 2024-10-17 01:11:41 +02:00
Nicolas Lepage
c93d379f20 feat: improves examples 2024-10-16 14:01:11 +02:00
Nicolas Lepage
98257b470a feat: allow using cache for wasm binary 2024-10-16 14:01:11 +02:00
Nicolas Lepage
b2bd8679fd fix: error management in readablestream writer close method 2024-10-16 13:19:16 +02:00
Nicolas Lepage
770d49a106 chore: v2.0.2 2024-10-14 23:30:16 +02:00
Nicolas Lepage
e8555180f7 fix: improves error management 2024-10-14 23:28:35 +02:00
Nicolas Lepage
8abad8cb77 fix: crash when request has no body 2024-10-14 23:28:21 +02:00
Nicolas Lepage
74cbaf89b5 fix: wrongful import path in v2.0.0 example 2024-10-14 22:56:59 +02:00
Nicolas Lepage
23cde9d811 chore: v2.0.0 2024-10-14 22:36:22 +02:00
Nicolas Lepage
b7e5adfd23 feat: uses ReadableStream for request (#16) 2024-10-14 22:23:22 +02:00
Nicolas Lepage
3220c94fa5 fix: hello-state-keepalive example crashes 2024-10-14 17:04:06 +02:00
Nicolas Lepage
5ec4a8d7e8 [FEATURE] Use ReadableStream for Response (#15)
* feat: uses ReadableStream for Response

* chore: rebuilds other examples
2024-10-14 09:14:50 +02:00
Nicolas Lepage
163b49702b ⬆️ go 1.18 compatibility 2022-07-28 00:08:56 +02:00
Nicolas Lepage
624ed00220 ♻️ Use github.com/nlepage/go-js-promise 2021-08-16 23:40:53 +02:00
Nicolas Lepage
1f549a4bf0 📝 Create funding.yml 2021-03-02 12:07:53 +01:00
Nicolas Lepage
73a09847ca ✏️ 2021-02-18 22:01:34 +01:00
Nicolas Lepage
0bf86b9d79 📝 2021-02-10 12:44:50 +01:00
Nicolas Lepage
167237a124 📝 2021-02-09 23:20:40 +01:00
Nicolas Lepage
f602159c47 📝 2021-02-09 23:18:59 +01:00
Nicolas Lepage
a06a85731f 📝 New Random password generator web server example 2021-02-08 13:54:49 +01:00
Nicolas Lepage
17e34981b0 📝 2021-02-06 18:15:36 +01:00
Nicolas Lepage
7434774930 📝 2021-01-31 22:24:30 +01:00
Nicolas Lepage
d2e039bd3e 📝 2021-01-31 21:59:22 +01:00
Nicolas Lepage
76abf72cff 📝 2021-01-28 16:00:27 +01:00
32 changed files with 981 additions and 197 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [nlepage]

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"go.toolsEnvVars": {
"GOOS": "js",
"GOARCH": "wasm"
}
}

162
README.md
View File

@@ -11,25 +11,159 @@
</a>
</p>
> Build your Go HTTP Server to [WebAssembly](https://mdn.io/WebAssembly/) and embed it in a ServiceWorker!
> Embed your Go HTTP handlers in a ServiceWorker (using [WebAssembly](https://mdn.io/WebAssembly/)) and emulate an HTTP server!
✨ [Demos](https://nlepage.github.io/go-wasm-http-server/)
## Examples
## Install
- [Hello example](https://nlepage.github.io/go-wasm-http-server/hello) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello))
- [Hello example with state](https://nlepage.github.io/go-wasm-http-server/hello-state) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state))
- [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))
TODO
## Usage
TODO
## Why?
TODO
## How?
TODO
Talk given at the Go devroom of FOSDEM 2021 explaining how `go-wasm-http-server` works:
[![Deploy a Go HTTP server in your browser Youtube link](https://raw.githubusercontent.com/nlepage/go-wasm-http-talk/main/youtube.png)](https://youtu.be/O2RB_8ircdE)
The slides are available [here](https://nlepage.github.io/go-wasm-http-talk/).
## Why?
`go-wasm-http-server` can help you put up a demonstration for a project without actually running a Go HTTP server.
## Requirements
`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)
## Usage
### Step 1: Build to `js/wasm`
In your Go code, replace [`http.ListenAndServe()`](https://pkg.go.dev/net/http#ListenAndServe) (or [`net.Listen()`](https://pkg.go.dev/net#Listen) + [`http.Serve()`](https://pkg.go.dev/net/http#Serve)) by [wasmhttp.Serve()](https://pkg.go.dev/github.com/nlepage/go-wasm-http-server#Serve):
📄 `server.go`
```go
// +build !js,!wasm
package main
import (
"net/http"
)
func main() {
// Define handlers...
http.ListenAndServe(":8080", nil)
}
```
becomes:
📄 `server_js_wasm.go`
```go
// +build js,wasm
package main
import (
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
func main() {
// Define handlers...
wasmhttp.Serve(nil)
}
```
You may want to use build tags as shown above (or file name suffixes) in order to be able to build both to WebAssembly and other targets.
Then build your WebAssembly binary:
```sh
GOOS=js GOARCH=wasm go build -o server.wasm .
```
### Step 2: Create ServiceWorker file
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')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.4/sw.js')
registerWasmHTTPListener('path/to/server.wasm')
```
By default the server will deploy at the ServiceWorker's scope root, check [`registerWasmHTTPListener()`'s API](https://github.com/nlepage/go-wasm-http-server#registerwasmhttplistenerwasmurl-options) for more information.
You may want to add these additional event listeners in your ServiceWorker:
```js
// Skip installed stage and jump to activating stage
addEventListener('install', (event) => {
event.waitUntil(skipWaiting())
})
// Start controlling clients as soon as the SW is activated
addEventListener('activate', event => {
event.waitUntil(clients.claim())
})
```
### Step 3: Register the ServiceWorker
In your web page(s), register the ServiceWorker:
```html
<script>
// By default the ServiceWorker's scope will be "server/"
navigator.serviceWorker.register('server/sw.js')
</script>
```
Now your web page(s) may start fetching from the server:
```js
// The server will receive a request for "/path/to/resource"
fetch('server/path/to/resource').then(res => {
// use response...
})
```
## API
For Go API see [pkg.go.dev/github.com/nlepage/go-wasm-http-server](https://pkg.go.dev/github.com/nlepage/go-wasm-http-server#section-documentation)
### JavaScript API
### `registerWasmHTTPListener(wasmUrl, options)`
Instantiates and runs the WebAssembly module at `wasmUrl`, and registers a fetch listener forwarding requests to the WebAssembly module's server.
⚠ This function must be called only once in a ServiceWorker, if you want to register several servers you must use several ServiceWorkers.
The server will be "deployed" at the root of the ServiceWorker's scope by default, `base` may be used to deploy the server at a subpath of the scope.
See [ServiceWorkerContainer.register()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) for more information about the scope of a ServiceWorker.
#### `wasmUrl`
URL string of the WebAssembly module, example: `"path/to/my-module.wasm"`.
#### `options`
An optional object containing:
- `base` (`string`): Base path of the server, relative to the ServiceWorker's scope.
- `args` (`string[]`): Arguments for the WebAssembly module.
## Author
@@ -53,4 +187,4 @@ Copyright © 2021 [Nicolas Lepage](https://github.com/nlepage).<br />
This project is [Apache 2.0](https://github.com/nlepage/go-wasm-http-server/blob/master/LICENSE) licensed.
***
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_

34
docs/hello-sse/api.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"net/http"
"time"
"github.com/tmaxmax/go-sse"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
func main() {
s := &sse.Server{}
t, _ := sse.NewType("ping")
go func() {
m := &sse.Message{
Type: t,
}
m.AppendData("Hello world")
for range time.Tick(time.Second) {
_ = s.Publish(m)
}
}()
http.Handle("/events", s)
if _, err := wasmhttp.Serve(nil); err != nil {
panic(err)
}
select {}
}

BIN
docs/hello-sse/api.wasm Executable file

Binary file not shown.

57
docs/hello-sse/index.html Normal file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>go-wasm-http-server hello sse demo</title>
<script>
navigator.serviceWorker.register('sw.js')
.then(registration => {
const serviceWorker = registration.installing ?? registration.waiting ?? registration.active
if (serviceWorker.state === 'activated') {
document.querySelector('#open-button').disabled = false
} else {
serviceWorker.addEventListener('statechange', e => {
if (e.target.state === 'activated') {
document.querySelector('#open-button').disabled = false
}
})
}
})
window.addEventListener('DOMContentLoaded', () => {
let es;
document.querySelector('#open-button').addEventListener('click', () => {
if (es && es.readyState === es.OPEN) return
es = new EventSource('api/events')
es.addEventListener('ping', (e) => {
const li = document.createElement('li')
li.textContent = `ping: data=${e.data}`
document.querySelector('ul').append(li)
})
})
document.querySelector('#close-button').addEventListener('click', () => {
if (!es) return
es.close()
})
document.querySelector('#clear-button').addEventListener('click', () => {
document.querySelector('ul').innerHTML = ''
})
window.addEventListener('unload', () => {
if (!es) return
es.close()
})
})
</script>
</head>
<body>
<p>
<button id="open-button" disabled>Open event source</button>
<button id="close-button">Close event source</button>
<button id="clear-button">Clear events</button>
</p>
<ul></ul>
</body>
</html>

14
docs/hello-sse/sw.js Normal file
View File

@@ -0,0 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.4/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' })

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>go-wasm-http-server hello with state demo</title>
<title>go-wasm-http-server hello with state and keepalive demo</title>
<script>
navigator.serviceWorker.register('sw.js').then(registration => {
const sw = registration.active ?? registration.installing ?? registration.waiting

View File

@@ -1,7 +1,10 @@
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@078ff3547ebe2abfbee1fd5af9ca5ad64be480c0/sw.js')
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js')
importScripts('../sw.js')
const wasm = '../hello-state/api.wasm'
addEventListener('install', event => {
event.waitUntil(skipWaiting())
event.waitUntil(caches.open('hello-state').then((cache) => cache.add(wasm)))
})
addEventListener('activate', event => {
@@ -10,4 +13,4 @@ addEventListener('activate', event => {
addEventListener('message', () => {})
registerWasmHTTPListener('../hello-state/api.wasm', { base: 'api' })
registerWasmHTTPListener(wasm, { base: 'api' })

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"sync/atomic"
wasmhttp "github.com/nlepage/go-wasm-http-server"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
func main() {

Binary file not shown.

View File

@@ -1,11 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@078ff3547ebe2abfbee1fd5af9ca5ad64be480c0/sw.js')
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.4/sw.js')
const wasm = 'api.wasm'
addEventListener('install', (event) => {
event.waitUntil(skipWaiting())
event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)))
})
addEventListener('activate', event => {
addEventListener('activate', (event) => {
event.waitUntil(clients.claim())
})
registerWasmHTTPListener('api.wasm', { base: 'api' })
registerWasmHTTPListener(wasm, { base: 'api' })

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
wasmhttp "github.com/nlepage/go-wasm-http-server"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
func main() {

Binary file not shown.

View File

@@ -1,11 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@078ff3547ebe2abfbee1fd5af9ca5ad64be480c0/sw.js')
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.4/sw.js')
const wasm = 'api.wasm'
addEventListener('install', (event) => {
event.waitUntil(skipWaiting())
event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)))
})
addEventListener('activate', event => {
addEventListener('activate', (event) => {
event.waitUntil(clients.claim())
})
registerWasmHTTPListener('api.wasm', { base: 'api' })
registerWasmHTTPListener(wasm, { base: 'api' })

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>go-wasm-http-server demo</title>
</head>
<body>
<ul>
<li><a href="hello">Hello demo</a> (<a href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello">sources</a>)</li>
<li><a href="hello-state">Hello with state demo</a> (<a href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state">sources</a>)</li>
<li><a href="hello-state-keepalive">Hello with state and keepalive demo</a> (<a href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state-keepalive">sources</a>)</li>
<li><a href="https://nlepage.github.io/catption/wasm/">😺 Catption generator demo</a> (<a href="https://github.com/nlepage/catption/tree/wasm">sources</a>)</li>
</ul>
</body>
</html>

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
wasmhttp "github.com/nlepage/go-wasm-http-server"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
// Demonstrates a simple hello JSON service.
@@ -23,7 +23,11 @@ func Example_json() {
}
})
defer wasmhttp.Serve(nil)()
release, err := wasmhttp.Serve(nil)
if err != nil {
panic(err)
}
defer release()
// Wait for webpage event or use empty select{}
}

10
go.mod
View File

@@ -1,3 +1,9 @@
module github.com/nlepage/go-wasm-http-server
module github.com/nlepage/go-wasm-http-server/v2
go 1.13
go 1.18
require (
github.com/hack-pad/safejs v0.1.1
github.com/nlepage/go-js-promise v1.0.0
github.com/tmaxmax/go-sse v0.8.0
)

6
go.sum Normal file
View File

@@ -0,0 +1,6 @@
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182epFwGQ=
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=

13
internal/jstype/types.go Normal file
View File

@@ -0,0 +1,13 @@
package jstype
import (
"syscall/js"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
var (
ReadableStream = safejs.Safe(js.Global().Get("ReadableStream"))
Response = safejs.Safe(js.Global().Get("Response"))
Uint8Array = safejs.Safe(js.Global().Get("Uint8Array"))
)

View File

@@ -0,0 +1,85 @@
package readablestream
import (
"io"
promise "github.com/nlepage/go-js-promise"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
type Reader struct {
value safejs.Value
buf []byte
off int
}
var _ io.Reader = (*Reader)(nil)
func NewReader(r safejs.Value) *Reader {
return &Reader{
value: r,
}
}
func (r *Reader) Read(p []byte) (int, error) {
if r.off < len(r.buf) {
n := copy(p, r.buf[r.off:])
r.off += n
return n, nil
}
r.off = 0
pRes, err := r.value.Call("read")
if err != nil {
return 0, err
}
ures, err := promise.Await(safejs.Unsafe(pRes))
if err != nil {
return 0, err
}
res := safejs.Safe(ures)
done, err := res.GetBool("done")
if err != nil {
return 0, err
}
if done {
return 0, io.EOF
}
value, err := res.Get("value")
if err != nil {
return 0, err
}
l, err := value.GetInt("length")
if err != nil {
return 0, err
}
if cap(r.buf) < l {
r.buf = make([]byte, l)
}
if len(r.buf) < cap(r.buf) {
r.buf = r.buf[:cap(r.buf)]
}
n, err := safejs.CopyBytesToGo(r.buf, value)
if err != nil {
return 0, err
}
r.buf = r.buf[:n]
n = copy(p, r.buf[r.off:])
r.off += n
return n, nil
}

View File

@@ -0,0 +1,103 @@
package readablestream
import (
"context"
"io"
"github.com/nlepage/go-wasm-http-server/v2/internal/jstype"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
type Writer struct {
Value safejs.Value
controller safejs.Value
ctx context.Context
cancelled bool
}
var _ io.WriteCloser = (*Writer)(nil)
func NewWriter() (*Writer, error) {
var start safejs.Func
var controller safejs.Value
start, err := safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any {
defer start.Release()
controller = args[0]
return nil
})
if err != nil {
return nil, err
}
var cancel safejs.Func
ctx, cancelCtx := context.WithCancel(context.Background())
cancel, err = safejs.FuncOf(func(_ safejs.Value, _ []safejs.Value) any {
defer cancel.Release()
cancelCtx()
return nil
})
if err != nil {
return nil, err
}
source, err := safejs.ValueOf(map[string]any{
"start": safejs.Unsafe(start.Value()),
"cancel": safejs.Unsafe(cancel.Value()),
})
if err != nil {
return nil, err
}
value, err := jstype.ReadableStream.New(source)
if err != nil {
return nil, err
}
rs := &Writer{
Value: value,
controller: controller,
ctx: ctx,
}
go func() {
<-ctx.Done()
rs.cancelled = true
}()
return rs, nil
}
func (rs *Writer) Write(b []byte) (int, error) {
if rs.cancelled {
return 0, nil
}
chunk, err := jstype.Uint8Array.New(len(b)) // FIXME reuse same Uint8Array?
if err != nil {
return 0, err
}
n, err := safejs.CopyBytesToJS(chunk, b)
if err != nil {
return 0, err
}
_, err = rs.controller.Call("enqueue", chunk)
return n, err
}
func (rs *Writer) Close() error {
if rs.cancelled {
return nil
}
_, err := rs.controller.Call("close")
return err
}
func (rs *Writer) Context() context.Context {
return rs.ctx
}

11
internal/safejs/bytes.go Normal file
View File

@@ -0,0 +1,11 @@
package safejs
import "github.com/hack-pad/safejs"
func CopyBytesToGo(dst []byte, src Value) (int, error) {
return safejs.CopyBytesToGo(dst, safejs.Value(src))
}
func CopyBytesToJS(dst Value, src []byte) (int, error) {
return safejs.CopyBytesToJS(safejs.Value(dst), src)
}

26
internal/safejs/func.go Normal file
View File

@@ -0,0 +1,26 @@
package safejs
import (
"github.com/hack-pad/safejs"
)
type Func safejs.Func
func FuncOf(fn func(this Value, args []Value) any) (Func, error) {
r, err := safejs.FuncOf(func(this safejs.Value, args []safejs.Value) any {
args2 := make([]Value, len(args))
for i, v := range args {
args2[i] = Value(v)
}
return fn(Value(this), []Value(args2))
})
return Func(r), err
}
func (f Func) Release() {
safejs.Func(f).Release()
}
func (f Func) Value() Value {
return Value(safejs.Func(f).Value())
}

111
internal/safejs/value.go Normal file
View File

@@ -0,0 +1,111 @@
package safejs
import (
"syscall/js"
"github.com/hack-pad/safejs"
)
type Value safejs.Value
func Safe(v js.Value) Value {
return Value(safejs.Safe(v))
}
func Unsafe(v Value) js.Value {
return safejs.Unsafe(safejs.Value(v))
}
func ValueOf(value any) (Value, error) {
v, err := safejs.ValueOf(value)
return Value(v), err
}
func (v Value) Call(m string, args ...any) (Value, error) {
args = toJSValue(args).([]any)
r, err := safejs.Value(v).Call(m, args...)
return Value(r), err
}
func (v Value) Get(p string) (Value, error) {
r, err := safejs.Value(v).Get(p)
return Value(r), err
}
func (v Value) GetBool(p string) (bool, error) {
bv, err := v.Get(p)
if err != nil {
return false, err
}
return safejs.Value(bv).Bool()
}
func (v Value) GetInt(p string) (int, error) {
iv, err := v.Get(p)
if err != nil {
return 0, err
}
return safejs.Value(iv).Int()
}
func (v Value) GetString(p string) (string, error) {
sv, err := v.Get(p)
if err != nil {
return "", err
}
return safejs.Value(sv).String()
}
func (v Value) Index(i int) (Value, error) {
r, err := safejs.Value(v).Index(i)
return Value(r), err
}
func (v Value) IndexString(i int) (string, error) {
sv, err := v.Index(i)
if err != nil {
return "", err
}
return safejs.Value(sv).String()
}
func (v Value) IsNull() bool {
return safejs.Value(v).IsNull()
}
func (v Value) IsUndefined() bool {
return safejs.Value(v).IsUndefined()
}
func (v Value) New(args ...any) (Value, error) {
args = toJSValue(args).([]any)
r, err := safejs.Value(v).New(args...)
return Value(r), err
}
func toJSValue(jsValue any) any {
switch value := jsValue.(type) {
case Value:
return safejs.Value(value)
case Func:
return safejs.Func(value)
case map[string]any:
newValue := make(map[string]any)
for mapKey, mapValue := range value {
newValue[mapKey] = toJSValue(mapValue)
}
return newValue
case []any:
newValue := make([]any, len(value))
for i, arg := range value {
newValue[i] = toJSValue(arg)
}
return newValue
default:
return jsValue
}
}

View File

@@ -1,4 +1,2 @@
// Package wasmhttp (github.com/nlepage/go-wasm-http-server) allows to create a WebAssembly Go HTTP Server embedded in a ServiceWorker.
//
// It is a subset of the full solution, a full usage is available on the github repository: https://github.com/nlepage/go-wasm-http-server
// Package wasmhttp allows to create a WebAssembly Go HTTP Server embedded in a ServiceWorker.
package wasmhttp

View File

@@ -1,55 +0,0 @@
package wasmhttp
import (
"syscall/js"
)
// NewPromise creates a new JavaScript Promise
func NewPromise() (p js.Value, resolve func(interface{}), reject func(interface{})) {
var cbFunc js.Func
cbFunc = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
cbFunc.Release()
resolve = func(value interface{}) {
args[0].Invoke(value)
}
reject = func(value interface{}) {
args[1].Invoke(value)
}
return js.Undefined()
})
p = js.Global().Get("Promise").New(cbFunc)
return
}
// Await waits for the Promise to be resolved and returns the value
func Await(p js.Value) (js.Value, error) {
resCh := make(chan js.Value)
var then js.Func
then = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
resCh <- args[0]
return nil
})
defer then.Release()
errCh := make(chan error)
var catch js.Func
catch = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
errCh <- js.Error{args[0]}
return nil
})
defer catch.Release()
p.Call("then", then).Call("catch", catch)
select {
case res := <-resCh:
return res, nil
case err := <-errCh:
return js.Undefined(), err
}
}

View File

@@ -1,33 +1,111 @@
package wasmhttp
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"syscall/js"
promise "github.com/nlepage/go-js-promise"
"github.com/nlepage/go-wasm-http-server/v2/internal/readablestream"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
// Request builds and returns the equivalent http.Request
func Request(r js.Value) *http.Request {
jsBody := js.Global().Get("Uint8Array").New(Await(r.Call("arrayBuffer")))
body := make([]byte, jsBody.Get("length").Int())
js.CopyBytesToGo(body, jsBody)
func Request(uvalue js.Value) (*http.Request, error) {
value := safejs.Safe(uvalue)
req := httptest.NewRequest(
r.Get("method").String(),
r.Get("url").String(),
bytes.NewBuffer(body),
)
headersIt := r.Get("headers").Call("entries")
for {
e := headersIt.Call("next")
if e.Get("done").Bool() {
break
}
v := e.Get("value")
req.Header.Set(v.Index(0).String(), v.Index(1).String())
method, err := value.GetString("method")
if err != nil {
return nil, err
}
return req
url, err := value.GetString("url")
if err != nil {
return nil, err
}
body, err := value.Get("body")
if err != nil {
return nil, err
}
var bodyReader io.Reader
if !body.IsNull() {
// WORKAROUND: Firefox does not have request.body ReadableStream
if body.IsUndefined() {
blobp, err := value.Call("blob")
if err != nil {
return nil, err
}
blob, err := promise.Await(safejs.Unsafe(blobp))
if err != nil {
return nil, err
}
body, err = safejs.Safe(blob).Call("stream")
if err != nil {
return nil, err
}
}
r, err := body.Call("getReader")
if err != nil {
return nil, err
}
bodyReader = readablestream.NewReader(r)
}
req := httptest.NewRequest(
method,
url,
bodyReader,
)
headers, err := value.Get("headers")
if err != nil {
return nil, err
}
headersIt, err := headers.Call("entries")
if err != nil {
return nil, err
}
for {
e, err := headersIt.Call("next")
if err != nil {
return nil, err
}
done, err := e.GetBool("done")
if err != nil {
return nil, err
}
if done {
break
}
v, err := e.Get("value")
if err != nil {
return nil, err
}
key, err := v.IndexString(0)
if err != nil {
return nil, err
}
value, err := v.IndexString(1)
if err != nil {
return nil, err
}
req.Header.Set(key, value)
}
return req, nil
}

175
response.go Normal file
View File

@@ -0,0 +1,175 @@
package wasmhttp
import (
"bufio"
"context"
"fmt"
"io"
"log/slog"
"net/http"
"syscall/js"
promise "github.com/nlepage/go-js-promise"
"github.com/nlepage/go-wasm-http-server/v2/internal/jstype"
"github.com/nlepage/go-wasm-http-server/v2/internal/readablestream"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
type Response interface {
http.ResponseWriter
io.StringWriter
http.Flusher
io.Closer
Context() context.Context
WriteError(string)
JSValue() js.Value
}
type response struct {
header http.Header
wroteHeader bool
promise js.Value
resolve func(any)
rs *readablestream.Writer
body *bufio.Writer
}
func NewResponse() (Response, error) {
rs, err := readablestream.NewWriter()
if err != nil {
return nil, err
}
promise, resolve, _ := promise.New()
return &response{
promise: promise,
resolve: resolve,
rs: rs,
body: bufio.NewWriter(rs),
}, nil
}
var _ Response = (*response)(nil)
// Header implements [http.ResponseWriter].
func (r *response) Header() http.Header {
if r.header == nil {
r.header = make(http.Header)
}
return r.header
}
func (r *response) headerValue() map[string]any {
h := r.Header()
hh := make(map[string]any, len(h)+1)
for k := range h {
hh[k] = h.Get(k)
}
return hh
}
// Write implements http.ResponseWriter.
func (r *response) Write(buf []byte) (int, error) {
r.writeHeader(buf, "")
return r.body.Write(buf)
}
// WriteHeader implements [http.ResponseWriter].
func (r *response) WriteHeader(code int) {
if r.wroteHeader {
return
}
checkWriteHeaderCode(code)
init, err := safejs.ValueOf(map[string]any{
"status": code,
"headers": r.headerValue(),
})
if err != nil {
panic(err)
}
res, err := jstype.Response.New(r.rs.Value, init)
if err != nil {
panic(err)
}
r.wroteHeader = true
r.resolve(safejs.Unsafe(res))
}
// WriteString implements [io.StringWriter].
func (r *response) WriteString(str string) (int, error) {
r.writeHeader(nil, str)
return r.body.WriteString(str)
}
// Flush implements [http.Flusher]
func (r *response) Flush() {
if !r.wroteHeader {
r.WriteHeader(200)
}
if err := r.body.Flush(); err != nil {
panic(err)
}
}
// Close implements [io.Closer]
func (r *response) Close() error {
if err := r.body.Flush(); err != nil {
return err
}
return r.rs.Close()
}
func (r *response) Context() context.Context {
return r.rs.Context()
}
func (r *response) WriteError(str string) {
slog.Error(str)
if !r.wroteHeader {
r.Header().Set("Content-Type", "text/plain")
r.WriteHeader(500)
_, _ = r.WriteString(str)
}
}
func (r *response) JSValue() js.Value {
return r.promise
}
func (r *response) writeHeader(b []byte, str string) {
if r.wroteHeader {
return
}
m := r.Header()
_, hasType := m["Content-Type"]
hasTE := m.Get("Transfer-Encoding") != ""
if !hasType && !hasTE {
if b == nil {
if len(str) > 512 {
str = str[:512]
}
b = []byte(str)
}
m.Set("Content-Type", http.DetectContentType(b))
}
r.WriteHeader(200)
}
func checkWriteHeaderCode(code int) {
if code < 100 || code > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
}
}

View File

@@ -1,50 +0,0 @@
package wasmhttp
import (
"io/ioutil"
"net/http/httptest"
"syscall/js"
)
// ResponseRecorder extends httptest.ResponseRecorder and implements js.Wrapper
type ResponseRecorder struct {
*httptest.ResponseRecorder
}
// NewResponseRecorder returns a new ResponseRecorder
func NewResponseRecorder() ResponseRecorder {
return ResponseRecorder{httptest.NewRecorder()}
}
var _ js.Wrapper = ResponseRecorder{}
// JSValue builds and returns the equivalent JS Response (implementing js.Wrapper)
func (rr ResponseRecorder) JSValue() js.Value {
var res = rr.Result()
var body js.Value = js.Undefined()
if res.ContentLength != 0 {
var b, err = ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
body = js.Global().Get("Uint8Array").New(len(b))
js.CopyBytesToJS(body, b)
}
var init = make(map[string]interface{}, 2)
if res.StatusCode != 0 {
init["status"] = res.StatusCode
}
if len(res.Header) != 0 {
var headers = make(map[string]interface{}, len(res.Header))
for k := range res.Header {
headers[k] = res.Header.Get(k)
}
init["headers"] = headers
}
return js.Global().Get("Response").New(body, init)
}

View File

@@ -1,55 +1,86 @@
package wasmhttp
import (
"context"
"fmt"
"net/http"
"strings"
"syscall/js"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
)
var (
wasmhttp = safejs.Safe(js.Global().Get("wasmhttp"))
)
// Serve serves HTTP requests using handler or http.DefaultServeMux if handler is nil.
func Serve(handler http.Handler) func() {
var h = handler
func Serve(handler http.Handler) (func(), error) {
h := handler
if h == nil {
h = http.DefaultServeMux
}
var prefix = js.Global().Get("wasmhttp").Get("path").String()
prefix, err := wasmhttp.GetString("path")
if err != nil {
return nil, err
}
for strings.HasSuffix(prefix, "/") {
prefix = strings.TrimSuffix(prefix, "/")
}
if prefix != "" {
var mux = http.NewServeMux()
mux := http.NewServeMux()
mux.Handle(prefix+"/", http.StripPrefix(prefix, h))
h = mux
}
var cb = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
var resPromise, resolve, reject = NewPromise()
handlerValue, err := safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) interface{} {
res, err := NewResponse()
if err != nil {
panic(err)
}
go func() {
ctx, cancel := context.WithCancel(res.Context())
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
reject(fmt.Sprintf("wasmhttp: panic: %+v\n", err))
} else {
reject(fmt.Sprintf("wasmhttp: panic: %v\n", r))
}
cancel()
}()
defer func() {
if err := res.Close(); err != nil {
panic(err)
}
}()
var res = NewResponseRecorder()
defer func() {
if r := recover(); r != nil {
res.WriteError(fmt.Sprintf("%+v", r))
}
}()
h.ServeHTTP(res, Request(args[0]))
req, err := Request(safejs.Unsafe(args[0]))
if err != nil {
res.WriteError(fmt.Sprintf("%+v", err))
return
}
resolve(res)
req = req.WithContext(ctx)
h.ServeHTTP(res, req)
}()
return resPromise
return res.JSValue()
})
if err != nil {
return nil, err
}
js.Global().Get("wasmhttp").Call("setHandler", cb)
if _, err = wasmhttp.Call("setHandler", handlerValue); err != nil {
return nil, err
}
return cb.Release
return handlerValue.Release, nil
}

9
sw.js
View File

@@ -1,6 +1,4 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.15.7/misc/wasm/wasm_exec.js')
function registerWasmHTTPListener(wasm, { base, args = [] } = {}) {
function registerWasmHTTPListener(wasm, { base, cacheName, args = [] } = {}) {
let path = new URL(registration.scope).pathname
if (base && base !== '') path = `${trimEnd(path, '/')}/${trimStart(base, '/')}`
@@ -13,7 +11,10 @@ function registerWasmHTTPListener(wasm, { base, args = [] } = {}) {
const go = new Go()
go.argv = [wasm, ...args]
WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(({ instance }) => go.run(instance))
const source = cacheName
? caches.open(cacheName).then((cache) => cache.match(wasm)).then((response) => response ?? fetch(wasm))
: caches.match(wasm).then(response => (response) ?? fetch(wasm))
WebAssembly.instantiateStreaming(source, go.importObject).then(({ instance }) => go.run(instance))
addEventListener('fetch', e => {
const { pathname } = new URL(e.request.url)