mirror of
https://github.com/nlepage/go-wasm-http-server.git
synced 2026-01-12 10:09:12 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74cbaf89b5 | ||
|
|
23cde9d811 | ||
|
|
b7e5adfd23 | ||
|
|
3220c94fa5 | ||
|
|
5ec4a8d7e8 | ||
|
|
163b49702b | ||
|
|
624ed00220 | ||
|
|
1f549a4bf0 | ||
|
|
73a09847ca | ||
|
|
0bf86b9d79 | ||
|
|
167237a124 | ||
|
|
f602159c47 | ||
|
|
a06a85731f | ||
|
|
17e34981b0 | ||
|
|
7434774930 | ||
|
|
d2e039bd3e | ||
|
|
76abf72cff |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [nlepage]
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"go.toolsEnvVars": {
|
||||
"GOOS": "js",
|
||||
"GOARCH": "wasm"
|
||||
}
|
||||
}
|
||||
162
README.md
162
README.md
@@ -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:
|
||||
|
||||
[](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.1/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
34
docs/hello-sse/api.go
Normal 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
BIN
docs/hello-sse/api.wasm
Executable file
Binary file not shown.
32
docs/hello-sse/index.html
Normal file
32
docs/hello-sse/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>go-wasm-http-server hello demo</title>
|
||||
<script>
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
.then(registration => {
|
||||
const serviceWorker = registration.installing ?? registration.waiting ?? registration.active
|
||||
if (serviceWorker.state === 'activated') {
|
||||
startEventSource()
|
||||
} else {
|
||||
serviceWorker.addEventListener('statechange', e => {
|
||||
if (e.target.state === 'activated') startEventSource()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function startEventSource() {
|
||||
const es = new EventSource('/api/events')
|
||||
es.addEventListener('ping', (e) => {
|
||||
const p = document.createElement('p')
|
||||
p.textContent = `ping: data=${e.data}`
|
||||
document.body.append(p)
|
||||
})
|
||||
window.addEventListener('unload', () => {
|
||||
es.close()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
12
docs/hello-sse/sw.js
Normal file
12
docs/hello-sse/sw.js
Normal file
@@ -0,0 +1,12 @@
|
||||
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.1/sw.js')
|
||||
|
||||
addEventListener('install', (event) => {
|
||||
event.waitUntil(skipWaiting())
|
||||
})
|
||||
|
||||
addEventListener('activate', event => {
|
||||
event.waitUntil(clients.claim())
|
||||
})
|
||||
|
||||
registerWasmHTTPListener('api.wasm', { base: 'api' })
|
||||
@@ -1,4 +1,5 @@
|
||||
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.1/sw.js')
|
||||
|
||||
addEventListener('install', event => {
|
||||
event.waitUntil(skipWaiting())
|
||||
|
||||
@@ -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.
@@ -1,4 +1,5 @@
|
||||
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.1/sw.js')
|
||||
|
||||
addEventListener('install', (event) => {
|
||||
event.waitUntil(skipWaiting())
|
||||
|
||||
@@ -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.
@@ -1,9 +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('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.1/sw.js')
|
||||
|
||||
addEventListener('install', (event) => {
|
||||
event.waitUntil(skipWaiting())
|
||||
})
|
||||
|
||||
|
||||
addEventListener('activate', event => {
|
||||
event.waitUntil(clients.claim())
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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
10
go.mod
@@ -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
6
go.sum
Normal 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
13
internal/jstype/types.go
Normal 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"))
|
||||
)
|
||||
85
internal/readablestream/reader.go
Normal file
85
internal/readablestream/reader.go
Normal 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
|
||||
}
|
||||
87
internal/readablestream/writer.go
Normal file
87
internal/readablestream/writer.go
Normal file
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return &Writer{
|
||||
Value: value,
|
||||
controller: controller,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rs *Writer) Write(b []byte) (int, error) {
|
||||
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 {
|
||||
rs.controller.Call("close")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *Writer) Context() context.Context {
|
||||
return rs.ctx
|
||||
}
|
||||
11
internal/safejs/bytes.go
Normal file
11
internal/safejs/bytes.go
Normal 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
26
internal/safejs/func.go
Normal 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())
|
||||
}
|
||||
103
internal/safejs/value.go
Normal file
103
internal/safejs/value.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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) 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
55
promise.go
55
promise.go
@@ -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
|
||||
}
|
||||
}
|
||||
92
request.go
92
request.go
@@ -1,33 +1,85 @@
|
||||
package wasmhttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"syscall/js"
|
||||
|
||||
"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())
|
||||
body, err := value.Get("body")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req
|
||||
r, err := body.Call("getReader")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method, err := value.GetString("method")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url, err := value.GetString("url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(
|
||||
method,
|
||||
url,
|
||||
readablestream.NewReader(r),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
174
response.go
Normal file
174
response.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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{
|
||||
"code": 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.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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
73
serve.go
73
serve.go
@@ -1,55 +1,92 @@
|
||||
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 {
|
||||
var errStr string
|
||||
if err, ok := r.(error); ok {
|
||||
errStr = err.Error()
|
||||
} else {
|
||||
errStr = fmt.Sprintf("%s", r)
|
||||
}
|
||||
res.WriteError(errStr)
|
||||
}
|
||||
}()
|
||||
|
||||
h.ServeHTTP(res, Request(args[0]))
|
||||
req, err := Request(safejs.Unsafe(args[0]))
|
||||
if err != nil {
|
||||
res.WriteError(err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
2
sw.js
2
sw.js
@@ -1,5 +1,3 @@
|
||||
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.15.7/misc/wasm/wasm_exec.js')
|
||||
|
||||
function registerWasmHTTPListener(wasm, { base, args = [] } = {}) {
|
||||
let path = new URL(registration.scope).pathname
|
||||
if (base && base !== '') path = `${trimEnd(path, '/')}/${trimStart(base, '/')}`
|
||||
|
||||
Reference in New Issue
Block a user