mirror of
https://github.com/syumai/workers.git
synced 2025-03-10 17:29:11 +00:00
Initial commit
This commit is contained in:
commit
bd1ab33600
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
19
LICENSE.md
Normal file
19
LICENSE.md
Normal 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
47
README.md
Normal 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
|
36
examples/assets/polyfill_performance.js
Normal file
36
examples/assets/polyfill_performance.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
543
examples/assets/wasm_exec.js
Normal file
543
examples/assets/wasm_exec.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
12
examples/simple-json-server/Makefile
Normal file
12
examples/simple-json-server/Makefile
Normal 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
|
52
examples/simple-json-server/README.md
Normal file
52
examples/simple-json-server/README.md
Normal 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
|
152
examples/simple-json-server/app/app_easyjson.go
Normal file
152
examples/simple-json-server/app/app_easyjson.go
Normal 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)
|
||||||
|
}
|
38
examples/simple-json-server/app/hello.go
Normal file
38
examples/simple-json-server/app/hello.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
12
examples/simple-json-server/go.mod
Normal file
12
examples/simple-json-server/go.mod
Normal 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
|
4
examples/simple-json-server/go.sum
Normal file
4
examples/simple-json-server/go.sum
Normal 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=
|
13
examples/simple-json-server/main.go
Normal file
13
examples/simple-json-server/main.go
Normal 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
|
||||||
|
}
|
17
examples/simple-json-server/worker.mjs
Normal file
17
examples/simple-json-server/worker.mjs
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
9
examples/simple-json-server/wrangler.toml
Normal file
9
examples/simple-json-server/wrangler.toml
Normal 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"
|
67
handler.go
Normal file
67
handler.go
Normal 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
32
jsutil.go
Normal 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
57
request.go
Normal 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
29
response.go
Normal 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
28
responsewriter.go
Normal 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
140
stream.go
Normal 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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user