WebAssembly with Go: Taking Web Apps to the Next Level | by Ege Aytin - Exotic Digital Access
  • Kangundo Road, Nairobi, Kenya
  • support@exoticdigitalaccess.co.ke
  • Opening Time : 07 AM - 10 PM
WebAssembly with Go: Taking Web Apps to the Next Level | by Ege Aytin

WebAssembly with Go: Taking Web Apps to the Next Level | by Ege Aytin

Let’s dive a bit deeper into the heart of our WebAssembly integration by exploring the key segments of our Go-based WASM code.

involves preparing and specifying our Go code to be compiled for a WebAssembly runtime.

// go:build wasm
// +build wasm

These lines serve as directives to the Go compiler, signaling that the following code is designated for a WebAssembly runtime environment. Specifically:

  • //go:build wasm: A build constraint ensuring the code is compiled only for WASM targets, adhering to modern syntax.
  • // +build wasm: An analogous constraint, utilizing older syntax for compatibility with prior Go versions.

In essence, these directives guide the compiler to include this code segment only when compiling for a WebAssembly architecture, ensuring an appropriate setup and function within this specific runtime.

package main

import (
"context"
"encoding/json"
"syscall/js"

"google.golang.org/protobuf/encoding/protojson"

"github.com/Permify/permify/pkg/development"
)

var dev *development.Development

func run() js.Func {
// The `run` function returns a new JavaScript function
// that wraps the Go function.
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {

// t will be used to store the unmarshaled JSON data.
// The use of an empty interface{} type means it can hold any type of value.
var t interface{}

// Unmarshal JSON from JavaScript function argument (args[0]) to Go's data structure (map).
// args[0].String() gets the JSON string from the JavaScript argument,
// which is then converted to bytes and unmarshaled (parsed) into the map `t`.
err := json.Unmarshal([]byte(args[0].String()), &t)

// If an error occurs during unmarshaling (parsing) the JSON,
// it returns an array with the error message "invalid JSON" to JavaScript.
if err != nil {
return js.ValueOf([]interface{}{"invalid JSON"})
}

// Attempt to assert that the parsed JSON (`t`) is a map with string keys.
// This step ensures that the unmarshaled JSON is of the expected type (map).
input, ok := t.(map[string]interface{})

// If the assertion is false (`ok` is false),
// it returns an array with the error message "invalid JSON" to JavaScript.
if !ok {
return js.ValueOf([]interface{}{"invalid JSON"})
}

// Run the main logic of the application with the parsed input.
// It’s assumed that `dev.Run` processes `input` in some way and returns any errors encountered during that process.
errors := dev.Run(context.Background(), input)

// If no errors are present (the length of the `errors` slice is 0),
// return an empty array to JavaScript to indicate success with no errors.
if len(errors) == 0 {
return js.ValueOf([]interface{}{})
}

// If there are errors, each error in the `errors` slice is marshaled (converted) to a JSON string.
// `vs` is a slice that will store each of these JSON error strings.
vs := make([]interface{}, 0, len(errors))

// Iterate through each error in the `errors` slice.
for _, r := range errors {
// Convert the error `r` to a JSON string and store it in `result`.
// If an error occurs during this marshaling, it returns an array with that error message to JavaScript.
result, err := json.Marshal(r)
if err != nil {
return js.ValueOf([]interface{}{err.Error()})
}
// Add the JSON error string to the `vs` slice.
vs = append(vs, string(result))
}

// Return the `vs` slice (containing all JSON error strings) to JavaScript.
return js.ValueOf(vs)
})
}

Within the realm of Permify, the run function stands as a cornerstone, executing a crucial bridging operation between JavaScript inputs and Go’s processing capabilities. It orchestrates real-time data interchange in JSON format, safeguarding that Permify’s core functionalities are smoothly and instantaneously accessible via a browser interface.

Digging into run:

  • JSON Data Interchange: Translating JavaScript inputs into a format utilizable by Go, the function unmarshals JSON, transferring data between JS and Go, assuring that the robust processing capabilities of Go can seamlessly manipulate browser-sourced inputs.
  • Error Handling: Ensuring clarity and user-awareness, it conducts meticulous error-checking during data parsing and processing, returning relevant error messages back to the JavaScript environment to ensure user-friendly interactions.
  • Contextual Processing: By employing dev.Run, it processes the parsed input within a certain context, managing application logic while handling potential errors to assure steady data management and user feedback.
  • Bidirectional Communication: As errors are marshaled back into JSON format and returned to JavaScript, the function ensures a two-way data flow, keeping both environments in synchronized harmony.

Thus, through adeptly managing data, error-handling, and ensuring a fluid two-way communication channel, run serves as an integral bridge, linking JavaScript and Go to ensure the smooth, real-time operation of Permify within a browser interface. This facilitation of interaction not only heightens user experience but also leverages the respective strengths of JavaScript and Go within the Permify environment.

// Continuing from the previously discussed code...

func main() {
// Instantiate a channel, 'ch', with no buffer, acting as a synchronization point for the goroutine.
ch := make(chan struct{}, 0)

// Create a new instance of 'Container' from the 'development' package and assign it to the global variable 'dev'.
dev = development.NewContainer()

// Attach the previously defined 'run' function to the global JavaScript object,
// making it callable from the JavaScript environment.
js.Global().Set("run", run())

// Utilize a channel receive expression to halt the 'main' goroutine, preventing the program from terminating.
<-ch
}

  1. ch := make(chan struct{}, 0): A synchronization channel is created to coordinate the activity of goroutines (concurrent threads in Go).
  2. dev = development.NewContainer(): Initializes a new container instance from the development package and assigns it to dev.
  3. js.Global().Set("run", run()): Exposes the Go run function to the global JavaScript context, enabling JavaScript to call Go functions.
  4. <-ch: Halts the main goroutine indefinitely, ensuring that the Go WebAssembly module remains active in the JavaScript environment.

In summary, the code establishes a Go environment running within WebAssembly that exposes specific functionality (run function) to the JavaScript side and keeps itself active and available for function calls from JavaScript.

Before we delve into Permify’s rich functionalities, it’s paramount to elucidate the steps of converting our Go code into a WASM module, priming it for browser execution.

For enthusiasts eager to delve deep into the complete Go codebase, don’t hesitate to browse our GitHub repository: Permify Wasm Code.

Kickstart the transformation of our Go application into a WASM binary with this command:

GOOS=js GOARCH=wasm go build -o permify.wasm main.go

This directive cues the Go compiler to churn out a .wasm binary attuned for JavaScript environments, with main.go as the source. The output, permify.wasm, is a concise rendition of our Go capabilities, primed for web deployment.

In conjunction with the WASM binary, the Go ecosystem offers an indispensable JavaScript piece named wasm_exec.js. It’s pivotal for initializing and facilitating our WASM module within a browser setting. You can typically locate this essential script inside the Go installation, under misc/wasm.

However, to streamline your journey, we’ve hosted wasm_exec.js right here for direct access: wasm_exec.

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

Equipped with these pivotal assets — the WASM binary and its companion JavaScript — the stage is set for its amalgamation into our frontend.

To kick things off, ensure you have a directory structure that clearly separates your WebAssembly-related code from the rest of your application. Based on your given structure, the loadWasm folder seems to be where all the magic happens:

loadWasm/

├── index.tsx // Your main React component that integrates WASM.
├── wasm_exec.js // Provided by Go, bridges the gap between Go's WASM and JS.
└── wasmTypes.d.ts // TypeScript type declarations for WebAssembly.

To view the complete structure and delve into the specifics of each file, refer to the Permify Playground on GitHub.

Inside the wasmTypes.d.ts, global type declarations are made which expand upon the Window interface to acknowledge the new methods brought in by Go’s WebAssembly:

declare global {
export interface Window {
Go: any;
run: (shape: string) => any[];
}
}
export {};

This ensures TypeScript recognizes the Go constructor and the run method when called on the global window object.

In index.tsx, several critical tasks are accomplished:

  • Import Dependencies: First off, we import the required JS and TypeScript declarations:
import "./wasm_exec.js";
import "./wasmTypes.d.ts";
  • WebAssembly Initialization: The asynchronous function loadWasm takes care of the entire process:
async function loadWasm(): Promise<void> {
const goWasm = new window.Go();
const result = await WebAssembly.instantiateStreaming(
fetch("play.wasm"),
goWasm.importObject
);
goWasm.run(result.instance);
}

Here, new window.Go() initializes the Go WASM environment. WebAssembly.instantiateStreaming fetches the WASM module, compiles it, and creates an instance. Finally, goWasm.run activates the WASM module.

  • React Component with Loader UI: The LoadWasm component uses the useEffect hook to asynchronously load the WebAssembly when the component mounts:
export const LoadWasm: React.FC<React.PropsWithChildren<{}>> = (props) => {
const [isLoading, setIsLoading] = React.useState(true);

useEffect(() => {
loadWasm().then(() => {
setIsLoading(false);
});
}, []);

if (isLoading) {
return (
<div className="wasm-loader-background h-screen">
<div className="center-of-screen">
<SVG src={toAbsoluteUrl("/media/svg/rocket.svg")} />
</div>
</div>
);
} else {
return <React.Fragment>{props.children}</React.Fragment>;
}
};

While loading, SVG rocket is displayed to indicate that initialization is ongoing. This feedback is crucial as users might otherwise be uncertain about what’s transpiring behind the scenes. Once loading completes, children components or content will render.

Given your Go WASM exposes a method named run, you can invoke it as follows:

function Run(shape) {
return new Promise((resolve) => {
let res = window.run(shape);
resolve(res);
});
}

This function essentially acts as a bridge, allowing the React frontend to communicate with the Go backend logic encapsulated in the WASM.

To integrate a button that triggers the WebAssembly function when clicked, follow these steps:

  1. Creating the Button Component

First, we’ll create a simple React component with a button:

import React from "react";

type RunButtonProps = {
shape: string;
onResult: (result: any[]) => void;
};

function RunButton({ shape, onResult }: RunButtonProps) {
const handleClick = async () => {
let result = await Run(shape);
onResult(result);
};

return <button onClick={handleClick}>Run WebAssembly</button>;
}

In the code above, the RunButton component accepts two props:

  • shape: The shape argument to pass to the WebAssembly run function.
  • onResult: A callback function that receives the result of the WebAssembly function and can be used to update the state or display the result in the UI.
  1. Integrating the Button in the Main Component

Now, in your main component (or wherever you’d like to place the button), integrate the RunButton:

import React, { useState } from "react";
import RunButton from "./path_to_RunButton_component"; // Replace with the actual path

function App() {
const [result, setResult] = useState<any[]>([]);

// Define the shape content
const shapeContent = {
schema: `|-
entity user {}

entity account {
relation owner @user
relation following @user
relation follower @user

attribute public boolean
action view = (owner or follower) or public
}

entity post {
relation account @account

attribute restricted boolean

action view = account.view

action comment = account.following not restricted
action like = account.following not restricted
}`,
relationships: [
"account:1#owner@user:kevin",
"account:2#owner@user:george",
"account:1#following@user:george",
"account:2#follower@user:kevin",
"post:1#account@account:1",
"post:2#account@account:2",
],
attributes: [
"account:1$public|boolean:true",
"account:2$public|boolean:false",
"post:1$restricted|boolean:false",
"post:2$restricted|boolean:true",
],
scenarios: [
{
name: "Account Viewing Permissions",
description:
"Evaluate account viewing permissions for 'kevin' and 'george'.",
checks: [
{
entity: "account:1",
subject: "user:kevin",
assertions: {
view: true,
},
},
],
},
],
};

return (
<div>
<RunButton shape={JSON.stringify(shapeContent)} onResult={setResult} />
<div>
Results:
<ul>
{result.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
</div>
);
}

In this example, App is a component that contains the RunButton. When the button is clicked, the result from the WebAssembly function is displayed in a list below the button.

Throughout this exploration, the integration of WebAssembly with Go was unfolded, illuminating the pathway toward enhanced web development and optimal user interactions within browsers.

The journey involved setting up the Go environment, converting Go code to WebAssembly, and executing it within a web context, ultimately giving life to the interactive platform showcased at play.permify.co.

This platform stands not only as an example but also as a beacon, illustrating the concrete and potent capabilities achievable when intertwining these technological domains.


Source link

Leave a Reply