Initial commit

This commit is contained in:
syumai 2022-05-18 00:04:37 +09:00
commit bd1ab33600
21 changed files with 1311 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

19
LICENSE.md Normal file
View File

@ -0,0 +1,19 @@
Copyright 2022-present [syumai](https://github.com/syumai/)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# workers
* `workers` is a package to run an HTTP server written in Go on [Cloudflare Workers](https://workers.cloudflare.com/).
* This package can easily serve *http.Handler* on Cloudflare Workers.
## Features
* [x] serve http.Handler
* [ ] environment variables (WIP)
* [ ] KV (WIP)
* [ ] R2 (WIP)
## Installation
```
go get github.com/syumai/workers
```
## Usage
implement your http.Handler and give it to `workers.Serve()`.
```go
func main() {
var handler http.HandlerFunc = func (w http.ResponseWriter, req *http.Request) { ... }
workers.Serve(handler)
}
```
or just call `http.Handle` and `http.HandleFunc`, then invoke `workers.Serve()` with nil.
```go
func main() {
http.HandleFunc("/hello", func (w http.ResponseWriter, req *http.Request) { ... })
workers.Serve(nil) // if nil is given, http.DefaultMux is used.
}
```
for concrete examples, see `examples` directory.
## License
MIT
## Author
syumai

View File

@ -0,0 +1,36 @@
// @license http://opensource.org/licenses/MIT
// copyright Paul Irish 2015
// Date.now() is supported everywhere except IE8. For IE8 we use the Date.now polyfill
// github.com/Financial-Times/polyfill-service/blob/master/polyfills/Date.now/polyfill.js
// as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values
// if you want values similar to what you'd get with real perf.now, place this towards the head of the page
// but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed
(function(){
if ("performance" in globalThis == false) {
globalThis.performance = {};
}
Date.now = (Date.now || function () { // thanks IE8
return new Date().getTime();
});
if ("now" in globalThis.performance == false){
var nowOffset = Date.now();
if (performance.timing && performance.timing.navigationStart){
nowOffset = performance.timing.navigationStart
}
globalThis.performance.now = function now(){
return Date.now() - nowOffset;
}
}
})();

View File

@ -0,0 +1,543 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// This file has been modified for use by the TinyGo compiler.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
/*
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
*/
/*
if (!global.fs && global.require) {
global.fs = require("fs");
}
*/
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!global.fs) {
let outputBuf = "";
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
/*
if (!global.crypto) {
const nodeCrypto = require("crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
*/
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
/*
if (!global.TextEncoder) {
global.TextEncoder = require("util").TextEncoder;
}
if (!global.TextDecoder) {
global.TextDecoder = require("util").TextDecoder;
}
*/
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
var logLine = [];
global.Go = class {
constructor() {
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.memory.buffer);
}
const setInt64 = (addr, v) => {
mem().setUint32(addr + 0, v, true);
mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const getInt64 = (addr) => {
const low = mem().getUint32(addr + 0, true);
const high = mem().getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = mem().getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = mem().getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number") {
if (isNaN(v)) {
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 0, true);
return;
}
if (v === 0) {
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 1, true);
return;
}
mem().setFloat64(addr, v, true);
return;
}
switch (v) {
case undefined:
mem().setFloat64(addr, 0, true);
return;
case null:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 2, true);
return;
case true:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 3, true);
return;
case false:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 4, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 1;
switch (typeof v) {
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
mem().setUint32(addr + 4, nanHead | typeFlag, true);
mem().setUint32(addr, id, true);
}
const loadSlice = (array, len, cap) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
}
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (ptr, len) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
wasi_snapshot_preview1: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
"proc_exit": (code) => {
if (global.process) {
// Node.js
process.exit(code);
} else {
// Can't exit in a browser.
throw 'trying to exit with code ' + code;
}
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
},
env: {
// func ticks() float64
"runtime.ticks": () => {
return timeOrigin + performance.now();
},
// func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(this._inst.exports.go_scheduler, timeout);
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
// Note: TinyGo does not support finalizers so this should never be
// called.
// console.error('syscall/js.finalizeRef not implemented');
},
// func stringVal(value string) ref
"syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => {
const s = loadString(value_ptr, value_len);
storeValue(ret_ptr, s);
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => {
let prop = loadString(p_ptr, p_len);
let value = loadValue(v_addr);
let result = Reflect.get(value, prop);
storeValue(retval, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => {
const v = loadValue(v_addr);
const p = loadString(p_ptr, p_len);
const x = loadValue(x_addr);
Reflect.set(v, p, x);
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (v_addr, p_ptr, p_len) => {
const v = loadValue(v_addr);
const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (ret_addr, v_addr, i) => {
storeValue(ret_addr, Reflect.get(loadValue(v_addr), i));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (v_addr, i, x_addr) => {
Reflect.set(loadValue(v_addr), i, loadValue(x_addr));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => {
const v = loadValue(v_addr);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const m = Reflect.get(v, name);
storeValue(ret_addr, Reflect.apply(m, v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => {
try {
const v = loadValue(v_addr);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
storeValue(ret_addr, Reflect.apply(v, undefined, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => {
const v = loadValue(v_addr);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
storeValue(ret_addr, Reflect.construct(v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr+ 8, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (v_addr) => {
return loadValue(v_addr).length;
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (ret_addr, v_addr) => {
const s = String(loadValue(v_addr));
const str = encoder.encode(s);
storeValue(ret_addr, str);
setInt64(ret_addr + 8, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => {
const str = loadValue(v_addr);
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (v_addr, t_addr) => {
return loadValue(v_addr) instanceof loadValue(t_addr);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadSlice(dest_addr, dest_len);
const src = loadValue(source_addr);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(num_bytes_copied_addr, toCopy.length);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
// copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified:
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
"syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadValue(dest_addr);
const src = loadSlice(source_addr, source_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(num_bytes_copied_addr, toCopy.length);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
}
};
}
async run(instance) {
this._inst = instance;
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
global,
this,
];
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
const mem = new DataView(this._inst.exports.memory.buffer)
while (true) {
const callbackPromise = new Promise((resolve) => {
this._resolveCallbackPromise = () => {
if (this.exited) {
throw new Error("bad callback: Go program has already exited");
}
setTimeout(resolve, 0); // make sure it is asynchronous
};
});
this._inst.exports._start();
if (this.exited) {
break;
}
await callbackPromise;
}
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
if (
global.require &&
global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
return go.run(result.instance);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})();

View File

@ -0,0 +1,12 @@
.PHONY: dev
dev:
wrangler dev
.PHONY: build
build:
mkdir -p dist
tinygo build -o ./dist/app.wasm -target wasm ./main.go
.PHONY: publish
publish:
wrangler publish

View File

@ -0,0 +1,52 @@
# simple-json-server
* A simple HTTP JSON server implemented in Go and compiled with tinygo.
## Example
* https://simple-json-server.syumai.workers.dev
### Request
```
curl --location --request POST 'https://simple-json-server.syumai.workers.dev/hello' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "syumai"
}'
```
### Response
```json
{
"message": "Hello, syumai!"
}
```
## Development
### Requirements
This project requires these tools to be installed globally.
* wrangler
* tinygo
* [easyjson](https://github.com/mailru/easyjson)
- `go install github.com/mailru/easyjson/...@latest`
### Commands
```
make dev # run dev server
make build # build Go Wasm binary
make publish # publish worker
```
## Author
syumai
## License
MIT

View File

@ -0,0 +1,152 @@
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package app
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp(in *jlexer.Lexer, out *HelloResponse) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "message":
out.Message = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp(out *jwriter.Writer, in HelloResponse) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"message\":"
out.RawString(prefix[1:])
out.String(string(in.Message))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v HelloResponse) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v HelloResponse) MarshalEasyJSON(w *jwriter.Writer) {
easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *HelloResponse) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *HelloResponse) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp(l, v)
}
func easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp1(in *jlexer.Lexer, out *HelloRequest) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "name":
out.Name = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp1(out *jwriter.Writer, in HelloRequest) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"name\":"
out.RawString(prefix[1:])
out.String(string(in.Name))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v HelloRequest) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp1(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v HelloRequest) MarshalEasyJSON(w *jwriter.Writer) {
easyjsonD2c14bEncodeGithubComSyumaiWorkersPlaygroundTinygoApp1(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *HelloRequest) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp1(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *HelloRequest) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjsonD2c14bDecodeGithubComSyumaiWorkersPlaygroundTinygoApp1(l, v)
}

View File

@ -0,0 +1,38 @@
//go:generate easyjson .
package app
import (
"fmt"
"net/http"
"os"
"github.com/mailru/easyjson"
)
//easyjson:json
type HelloRequest struct {
Name string `json:"name"`
}
//easyjson:json
type HelloResponse struct {
Message string `json:"message"`
}
func HelloHandler(w http.ResponseWriter, req *http.Request) {
var helloReq HelloRequest
if err := easyjson.UnmarshalFromReader(req.Body, &helloReq); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("request format is invalid"))
return
}
w.Header().Set("Content-Type", "application/json")
msg := fmt.Sprintf("Hello, %s!", helloReq.Name)
helloRes := HelloResponse{Message: msg}
if _, err := easyjson.MarshalToWriter(&helloRes, w); err != nil {
fmt.Fprintf(os.Stderr, "failed to encode response: %w\n", err)
}
}

View File

@ -0,0 +1,12 @@
module github.com/syumai/workers/examples/simple-json-server
go 1.18
require (
github.com/mailru/easyjson v0.7.7
github.com/syumai/workers v0.0.0-00010101000000-000000000000
)
replace github.com/syumai/workers => ../../
require github.com/josharian/intern v1.0.0 // indirect

View File

@ -0,0 +1,4 @@
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=

View File

@ -0,0 +1,13 @@
package main
import (
"net/http"
"github.com/syumai/workers"
"github.com/syumai/workers/examples/simple-json-server/app"
)
func main() {
http.HandleFunc("/hello", app.HelloHandler)
workers.Serve(nil) // use http.DefaultServeMux
}

View File

@ -0,0 +1,17 @@
import "../assets/polyfill_performance.js";
import "../assets/wasm_exec.js";
import mod from "./dist/app.wasm";
const go = new Go();
const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => {
go.run(instance);
return instance;
});
export default {
async fetch(req) {
await load;
return handleRequest(req);
},
};

View File

@ -0,0 +1,9 @@
name = "simple-json-server"
main = "./worker.mjs"
compatibility_date = "2022-05-13"
compatibility_flags = [
"streams_enable_constructors"
]
[build]
command = "make build"

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/syumai/workers
go 1.18

67
handler.go Normal file
View File

@ -0,0 +1,67 @@
package workers
import (
"fmt"
"io"
"net/http"
"syscall/js"
)
var httpHandler http.Handler
func init() {
var handleRequestCallback js.Func
handleRequestCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
panic(fmt.Errorf("too many args given to handleRequest: %d", len(args)))
}
var cb js.Func
cb = js.FuncOf(func(_ js.Value, pArgs []js.Value) any {
defer cb.Release()
resolve := pArgs[0]
go func() {
res, err := handleRequest(args[0])
if err != nil {
panic(err)
}
resolve.Invoke(res)
}()
return js.Undefined()
})
return newPromise(cb)
})
global.Set("handleRequest", handleRequestCallback)
}
// handleRequest accepts a Request object and returns Response object.
func handleRequest(reqObj js.Value) (js.Value, error) {
if httpHandler == nil {
return js.Value{}, fmt.Errorf("Serve must be called before handleRequest.")
}
req, err := toRequest(reqObj)
if err != nil {
panic(err)
}
reader, writer := io.Pipe()
w := &responseWriterBuffer{
header: http.Header{},
statusCode: http.StatusOK,
PipeReader: reader,
PipeWriter: writer,
}
go func() {
defer writer.Close()
httpHandler.ServeHTTP(w, req)
}()
return w.toJSResponse()
}
// Server serves http.Handler on Cloudflare Workers.
// if the given handler is nil, http.DefaultServeMux will be used.
func Serve(handler http.Handler) {
if handler == nil {
handler = http.DefaultServeMux
}
httpHandler = handler
select {}
}

32
jsutil.go Normal file
View File

@ -0,0 +1,32 @@
package workers
import "syscall/js"
var (
global = js.Global()
objectClass = global.Get("Object")
promiseClass = global.Get("Promise")
responseClass = global.Get("Response")
headersClass = global.Get("Headers")
arrayClass = global.Get("Array")
uint8ArrayClass = global.Get("Uint8Array")
errorClass = global.Get("Error")
readableStreamClass = global.Get("ReadableStream")
)
func newObject() js.Value {
return objectClass.New()
}
func newUint8Array(size int) js.Value {
return uint8ArrayClass.New(size)
}
func newPromise(fn js.Func) js.Value {
return promiseClass.New(fn)
}
// arrayFrom calls Array.from to given argument and returns result Array.
func arrayFrom(v js.Value) js.Value {
return arrayClass.Call("from", v)
}

57
request.go Normal file
View File

@ -0,0 +1,57 @@
package workers
import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"syscall/js"
)
// toBody converts JavaScripts sides ReadableStream (can be null) to io.ReadCloser.
// * ReadableStream: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
func toBody(streamOrNull js.Value) io.ReadCloser {
if streamOrNull.IsNull() {
return nil
}
sr := streamOrNull.Call("getReader")
return io.NopCloser(convertStreamReaderToReader(sr))
}
// toHeader converts JavaScript sides Headers to http.Header.
// * Headers: https://developer.mozilla.org/ja/docs/Web/API/Headers
func toHeader(headers js.Value) http.Header {
entries := arrayFrom(headers.Call("entries"))
headerLen := entries.Length()
h := http.Header{}
for i := 0; i < headerLen; i++ {
entry := entries.Index(i)
key := entry.Index(0).String()
value := entry.Index(1).String()
h[key] = strings.Split(value, ",")
}
return h
}
// toRequest converts JavaScript sides Request to *http.Request.
// * Request: https://developer.mozilla.org/ja/docs/Web/API/Request
func toRequest(req js.Value) (*http.Request, error) {
reqUrl, err := url.Parse(req.Get("url").String())
if err != nil {
return nil, err
}
header := toHeader(req.Get("headers"))
// ignore err
contentLength, _ := strconv.ParseInt(header.Get("Content-Length"), 10, 64)
return &http.Request{
Method: req.Get("method").String(),
URL: reqUrl,
Header: header,
Body: toBody(req.Get("body")),
ContentLength: contentLength,
TransferEncoding: strings.Split(header.Get("Transfer-Encoding"), ","),
Host: header.Get("Host"),
}, nil
}

29
response.go Normal file
View File

@ -0,0 +1,29 @@
package workers
import (
"io"
"net/http"
"syscall/js"
)
func toJSHeader(header http.Header) js.Value {
h := headersClass.New()
for key, values := range header {
for _, value := range values {
h.Call("append", key, value)
}
}
return h
}
func toJSResponse(body io.ReadCloser, status int, header http.Header) (js.Value, error) {
if status == 0 {
status = http.StatusOK
}
respInit := newObject()
respInit.Set("status", status)
respInit.Set("statusText", http.StatusText(status))
respInit.Set("headers", toJSHeader(header))
readableStream := convertReaderToReadableStream(body)
return responseClass.New(readableStream, respInit), nil
}

28
responsewriter.go Normal file
View File

@ -0,0 +1,28 @@
package workers
import (
"io"
"net/http"
"syscall/js"
)
type responseWriterBuffer struct {
header http.Header
statusCode int
*io.PipeReader
*io.PipeWriter
}
var _ http.ResponseWriter = &responseWriterBuffer{}
func (w responseWriterBuffer) Header() http.Header {
return w.header
}
func (w responseWriterBuffer) WriteHeader(statusCode int) {
w.statusCode = statusCode
}
func (w responseWriterBuffer) toJSResponse() (js.Value, error) {
return toJSResponse(w.PipeReader, w.statusCode, w.header)
}

140
stream.go Normal file
View File

@ -0,0 +1,140 @@
package workers
import (
"bytes"
"fmt"
"io"
"syscall/js"
)
// streamReaderToReader 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
type streamReaderToReader struct {
buf bytes.Buffer
streamReader js.Value
}
// Read reads bytes from ReadableStreamDefaultReader.
func (sr *streamReaderToReader) Read(p []byte) (n int, err error) {
if sr.buf.Len() == 0 {
promise := sr.streamReader.Call("read")
resultCh := make(chan js.Value)
errCh := make(chan error)
var then, catch js.Func
then = js.FuncOf(func(_ js.Value, args []js.Value) any {
defer then.Release()
result := args[0]
if result.Get("done").Bool() {
errCh <- io.EOF
return js.Undefined()
}
resultCh <- result.Get("value")
return js.Undefined()
})
catch = js.FuncOf(func(_ js.Value, args []js.Value) any {
defer catch.Release()
result := args[0]
errCh <- fmt.Errorf("JavaScript error on read: %s", result.Call("toString").String())
return js.Undefined()
})
promise.Call("then", then).Call("catch", catch)
select {
case result := <-resultCh:
chunk := make([]byte, result.Get("byteLength").Int())
_ = js.CopyBytesToGo(chunk, result)
// The length written is always the same as the length of chunk, so it can be discarded.
// - https://pkg.go.dev/bytes#Buffer.Write
_, err := sr.buf.Write(chunk)
if err != nil {
return 0, err
}
case err := <-errCh:
return 0, err
}
}
return sr.buf.Read(p)
}
// convertStreamReaderToReader converts ReadableStreamDefaultReader to io.Reader.
func convertStreamReaderToReader(sr js.Value) io.Reader {
return &streamReaderToReader{
streamReader: sr,
}
}
// readerToReadableStream implements ReadableStream sourced from io.ReadCloser.
// * ReadableStream: https://developer.mozilla.org/docs/Web/API/ReadableStream
// * This implementation is based on: https://deno.land/std@0.139.0/streams/conversion.ts#L230
type readerToReadableStream struct {
reader io.ReadCloser
chunkBuf []byte
}
// Pull implements ReadableStream's pull method.
// * https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#pull
func (rs *readerToReadableStream) Pull(controller js.Value) error {
n, err := rs.reader.Read(rs.chunkBuf)
if err == io.EOF {
if err := rs.reader.Close(); err != nil {
return err
}
controller.Call("close")
return nil
}
if err != nil {
jsErr := errorClass.New(err.Error())
controller.Call("error", jsErr)
if err := rs.reader.Close(); err != nil {
return err
}
return err
}
ua := newUint8Array(n)
_ = js.CopyBytesToJS(ua, rs.chunkBuf[:n])
controller.Call("enqueue", ua)
return nil
}
// Cancel implements ReadableStream's cancel method.
// * https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#cancel
func (rs *readerToReadableStream) Cancel() error {
return rs.reader.Close()
}
// https://deno.land/std@0.139.0/streams/conversion.ts#L5
const defaultChunkSize = 16_640
// convertReaderToReadableStream converts io.ReadCloser to ReadableStream.
func convertReaderToReadableStream(reader io.ReadCloser) js.Value {
stream := &readerToReadableStream{
reader: reader,
chunkBuf: make([]byte, defaultChunkSize),
}
rsInit := newObject()
rsInit.Set("pull", js.FuncOf(func(_ js.Value, args []js.Value) any {
var cb js.Func
cb = js.FuncOf(func(this js.Value, pArgs []js.Value) any {
defer cb.Release()
resolve := pArgs[0]
reject := pArgs[1]
controller := args[0]
err := stream.Pull(controller)
if err != nil {
reject.Invoke(errorClass.New(err.Error()))
return js.Undefined()
}
resolve.Invoke()
return js.Undefined()
})
return newPromise(cb)
}))
rsInit.Set("cancel", js.FuncOf(func(js.Value, []js.Value) any {
err := stream.Cancel()
if err != nil {
panic(err)
}
return js.Undefined()
}))
return readableStreamClass.New(rsInit)
}