diff --git a/example/basic/src/api/bridge.go b/example/basic/src/api/bridge.go
new file mode 100644
index 0000000..bca3fb5
--- /dev/null
+++ b/example/basic/src/api/bridge.go
@@ -0,0 +1,79 @@
+package main
+
+import "syscall/js"
+
+var (
+ // bridgeName is the namesace for all functions and values set.
+ //
+ // The returning JavaScript proxy via the webpack loader will look for functions and values under this namespace.
+ bridgeName = "__go_wasm__"
+
+ // The JS object of the __go_wasm__ value.
+ bridge js.Value
+
+ // Wrapper is a simple JS function that when called with a Go Function, will return a new function that will throw
+ // if the property `error` is an instance of JavaScript's `error`.
+ //
+ // All Go functions in the bridgeName proxy are expected to be the result of calling wrapper with the Go function.
+ wrapper js.Value
+)
+
+// newReturnValue creates an object with the value as the result.
+// See wrapGoFunc for the reasoning behind style style of returning values from Go functions.
+func newReturnValue(value interface{}) js.Value {
+ jsObject := js.Global().Get("Object").New()
+ jsObject.Set("result", value)
+
+ return jsObject
+}
+
+// newReturnError creates an object with the goError's message and creates a Javascript Error object with the message.
+//
+// See wrapGoFunc for the reasoning behind style style of returning values from Go functions.
+func newReturnError(goErr error) js.Value {
+ jsObject := js.Global().Get("Object").New()
+ jsError := js.Global().Get("Error")
+ jsObject.Set("error", jsError.New(goErr.Error()))
+
+ return jsObject
+}
+
+// Using this wrapper makes it possible to throw errors in go-fashion.
+// This means that all wrapped functions must return value and an error (respectively).
+//
+// The __wrapper__ function from JS will automatically throw if the returned object has an 'error' property.
+// Inversely, it will automatically give the result value if that property exists.
+// All Go functions directly returned via wasm should keep this in mind.
+func wrapGoFunc(f func(js.Value, []js.Value) (interface{}, error)) js.Func {
+ return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ res, err := f(this, args)
+ if err != nil {
+ return newReturnError(err)
+ }
+
+ return newReturnValue(res)
+ })
+}
+
+func setFunc(name string, f func(js.Value, []js.Value) (interface{}, error)) {
+ bridge.Set(name, wrapper.Invoke(wrapGoFunc(f)))
+}
+
+func setValue(name string, value interface{}) {
+ bridge.Set(name, value)
+}
+
+// Toggling the __ready__ value in the bridge lets JS know that everything is setup.
+// Setting __ready__ to true can help prevent possible race conditions of Wasm being called before everything is
+// registered, and potentially crashing applications.
+func ready() {
+ bridge.Set("__ready__", true)
+ <-make(chan bool, 0) // To use anything from Go WASM, the program may not exit.
+}
+
+// We want to make sure that this is always ran first. This means that we can make sure that whenever functions are
+// initialized, they are able to be set to the bridge and wrapper.
+func init() {
+ bridge = js.Global().Get(bridgeName)
+ wrapper = bridge.Get("__wrapper__")
+}
diff --git a/example/basic/src/api/main.go b/example/basic/src/api/main.go
index 5e6c8bf..75090c5 100644
--- a/example/basic/src/api/main.go
+++ b/example/basic/src/api/main.go
@@ -5,27 +5,24 @@ import (
"syscall/js"
)
-func main() {
- fmt.Println("Hello from go-mod-wasm!")
- setup()
-
- c := make(chan bool, 0) // To use anything from Go WASM, the program may not exit.
- <-c
-}
-
-const hello = "Sample value"
-
-func helloName(_ js.Value, args []js.Value) interface{} {
- return fmt.Sprintf("Hello, %s!", args[0].String())
+const hello = "Hello!"
+
+// helloName's first value is JavaScript's `this`.
+// However, the way that the JS bridge is written, it will always be JavaScript's undefined.
+//
+// If returning a non-nil error value, the resulting promise will be rejected by API consumers.
+// The rejected value will JavaScript's Error, with the message being the go error's message.
+//
+// See other examples which use the Go WASM bridge api, which show more flexibility and type safety when interacting
+// with JavaScript.
+func helloName(_ js.Value, args []js.Value) (interface{}, error) {
+ return fmt.Sprintf("Hello, %s!", args[0].String()), nil
}
-func setup() {
- bridge := js.Global().Get("__go_wasm__")
-
- bridge.Set("__ready__", true)
-
- bridge.Set("hello", hello)
- bridge.Set("helloName", js.FuncOf(helloName))
+func main() {
+ fmt.Println("golang-wasm initialized")
- js.Global()
+ setFunc("helloName", helloName)
+ setValue("hello", hello)
+ ready()
}
diff --git a/example/basic/src/index.js b/example/basic/src/index.js
index b491452..020dbc6 100644
--- a/example/basic/src/index.js
+++ b/example/basic/src/index.js
@@ -2,7 +2,17 @@ import wasm from './api/main.go';
const { hello, helloName } = wasm;
-(async () => {
- console.log(await hello());
- console.log(await helloName("world"));
-})()
\ No newline at end of file
+const value = document.getElementById("value");
+const input = document.getElementById("input");
+const funcValue = document.getElementById("funcValue");
+
+const run = async () => {
+ value.innerText = await hello();
+
+ funcValue.innerText = await helloName(input.value);
+ input.addEventListener("keyup", async (e) => {
+ funcValue.innerText = await helloName(e.target.value);
+ })
+}
+
+run()
\ No newline at end of file
diff --git a/package.json b/package.json
index b5d9165..4d2f981 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
- "name": "go-mod-wasm",
- "version": "0.1.0",
+ "name": "golang-wasm",
+ "version": "0.0.1",
"description": "A webpack-based configuration to work with wasm using Go.",
"main": "src/index.js",
"repository": {
"type": "git",
- "url": "https://gitea.teamortix.com/Team-Ortix/go-mod-wasm"
+ "url": "https://gitea.teamortix.com/Team-Ortix/golang-wasm"
},
"keywords": [
"golang",
diff --git a/src/bridge.js b/src/bridge.js
index 6073124..4c85da6 100644
--- a/src/bridge.js
+++ b/src/bridge.js
@@ -1,20 +1,31 @@
+// Initially, the __go_wasm__ object will be an empty object.
const g = global || window || self;
if (!g.__go_wasm__) {
g.__go_wasm__ = {};
}
+/**
+ * The maximum amount of time that we would expect Wasm to take to initialize.
+ * If it doesn't initialize after this time, we send a warning to console.
+ * Most likely something has gone wrong if it takes more than 3 seconds to initialize.
+ */
const maxTime = 3 * 1000;
+/**
+ * bridge is an easier way to refer to the Go WASM object.
+ */
const bridge = g.__go_wasm__;
-
/**
* Wrapper is used by Go to run all Go functions in JS.
- * Go functions always return an object of the following spec:
+ *
+ * @param {Function} goFunc a function that is expected to return an object of the following specification:
* {
* result: undefined | any // undefined when error is returned, or function returns undefined
* error: Error | undefined // undefined when no error is present
* }
+ *
+ * @returns {Function} returns a function that take arguments which are used to call the Go function.
*/
function wrapper(goFunc) {
return (...args) => {
@@ -25,28 +36,52 @@ function wrapper(goFunc) {
return result.result;
}
}
-bridge.__wrapper__ = wrapper
+/**
+ * Sleep is used when awaiting for Go Wasm to initialize.
+ * It uses the lowest possible sane delay time (via requestAnimationFrame).
+ * However, if the window is not focused, requestAnimationFrame never returns.
+ * A timeout will ensure to be called after 50 ms, regardless of whether or not the tab is in focus.
+ *
+ * @returns {Promise} an always-resolving promise when a tick has been completed
+ */
function sleep() {
- return new Promise(requestAnimationFrame);
+ return new Promise((res) => {
+ requestAnimationFrame(() => res());
+ setTimeout(() => {
+ res();
+ }, 50);
+ });
}
+
+/**
+ * @param {ArrayBuffer} getBytes a promise that is bytes of the Go Wasm object.
+ *
+ * @returns {Proxy} an object that can be used to call Wasm's objects and properly parse their results.
+ *
+ * All values that want to be retrieved from the proxy, regardless of if they are a function or not, must be retrieved
+ * as if they are from a function call.
+ *
+ * If a non-function value is returned however arguments are provided, a warning will be printed.
+ */
export default function (getBytes) {
let proxy;
async function init() {
+ bridge.__wrapper__ = wrapper;
+
const go = new g.Go();
let bytes = await getBytes;
let result = await WebAssembly.instantiate(bytes, go.importObject);
go.run(result.instance);
- bridge.__proxy__ = proxy
- setTimeout(() => {
- if (bridge.__ready__ !== true) {
- console.warn("Golang Wasm Bridge (__go_wasm__.__ready__) still not true after max time");
- }
- }, maxTime);
}
init();
+ setTimeout(() => {
+ if (bridge.__ready__ !== true) {
+ console.warn("Golang WASM Bridge (__go_wasm__.__ready__) still not true after max time");
+ }
+ }, maxTime);
proxy = new Proxy(
@@ -61,13 +96,17 @@ export default function (getBytes) {
if (typeof bridge[key] !== 'function') {
res(bridge[key]);
+
+ if (args.length !== 0) {
+ console.warn("Retrieved value from WASM returned non-error type, however called with arguments.")
+ }
return;
}
try {
res(bridge[key].apply(undefined, args));
} catch (e) {
- rej(e)
+ rej(e);
}
})
};
@@ -75,5 +114,6 @@ export default function (getBytes) {
}
);
+ bridge.__proxy__ = proxy;
return proxy;
}
diff --git a/src/index.js b/src/index.js
index d12ff1c..a68600d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -43,11 +43,14 @@ module.exports = function (source) {
}
modDir = path.join(modDir, "..");
}
-
if (!found) {
return cb(new Error("Could not find go.mod in any parent directory of " + this.resourcePath));
}
+ // Having context dependency before compilation means if any file apart from the imported one fails in
+ // compilation, the updates are still watched.
+ this.addContextDependency(modDir);
+
const wasmOrigPath = path.join(process.env.GOROOT, "misc", "wasm", "wasm_exec.js");
const wasmSavePath = path.join(__dirname, 'wasm_exec.js');
const errorPaths = ["\t" + wasmOrigPath, "\t" + wasmSavePath];
@@ -73,7 +76,6 @@ module.exports = function (source) {
const emitPath = path.basename(outFile);
this.emitFile(emitPath, contents);
- this.addContextDependency(modDir);
cb(null,
`require('!${wasmSavePath}');