[SDK-55] Create a React SDK
Michael-F-Bryan opened this issue · comments
As part of the ffmpeg demo (#361), I introduced a bunch of utilities for making wasmer/sdk easier to use inside a React application.
hooks.tsx
import React, { useContext, useEffect, useState } from "react";
import { Command, Runtime, Wasmer, init, initializeLogger } from "@wasmer/sdk";
export type WasmerSdkState = { state: "loading" } | { state: "loaded" } | { state: "error", error: any };
export type WasmerSdkProps = {
/**
* The filter passed to {@link initializeLogger}.
*/
log?: string,
wasm?: Parameters<typeof init>[0],
children: React.ReactElement,
}
const Context = React.createContext<WasmerSdkState | null>(null);
// Note: The wasm-bindgen glue code only needs to be initialized once, and
// initializing the logger multiple times will throw an exception, so we use a
// global variable to keep track of the in-progress initialization.
let pending: Promise<void> | undefined = undefined;
/**
* A wrapper component which will automatically initialize the Wasmer SDK.
*/
export function WasmerSdk(props?: WasmerSdkProps) {
const [state, setState] = useState<WasmerSdkState>();
useEffect(() => {
if (typeof pending == "undefined") {
pending = init(props?.wasm).then(() => initializeLogger(props?.log));
}
pending
.then(() => setState({ state: "loaded" }))
.catch(e => setState({ state: "error", error: e }));
}, [])
return (
<Context.Provider value={state || { state: "loading" }}>
{props?.children}
</Context.Provider>
)
}
export function useWasmerSdk(): WasmerSdkState {
const ctx = useContext(Context);
if (ctx == null) {
throw new Error("Attempting to use the Wasmer SDK outside of a <WasmerSDK /> component");
}
return ctx;
}
type LoadingPackageState =
{ state: "loading-package" }
| {
state: "loaded", pkg: Wasmer,
commands: Record<string, Command>,
entrypoint?: Command,
}
| { state: "error", error: any };
export type UseWasmerPackageState =
| { state: "loading-sdk" }
| { state: "sdk-error", error: any }
| LoadingPackageState;
export function useWasmerPackage(pkg: string | Uint8Array, runtime?: Runtime): UseWasmerPackageState {
const sdk = useWasmerSdk();
const [state, setState] = useState<LoadingPackageState>();
// We can't do anything until the SDK has been loaded
switch (sdk.state) {
case "error":
return { state: "sdk-error", error: sdk.error };
case "loading":
return { state: "loading-sdk" };
}
if (typeof state != "undefined") {
return state;
}
const newState = { state: "loading-package" } as const;
setState(newState);
const pending = (typeof pkg == "string")
? Wasmer.fromRegistry(pkg, runtime)
: Wasmer.fromFile(pkg, runtime);
pending
.then(pkg => {
setState({ state: "loaded", pkg, commands: pkg.commands, entrypoint: pkg.entrypoint });
})
.catch(error => setState({ state: "error", error }));
return newState;
}
If people want to initialize the JavaScript SDK, they can wrap their app in a <WasmerSdk>
tag then use useWasmerSdk()
to monitor the loading state or useWasmerPackage()
to load a package.
We should extract this into its own @wasmer/react-sdk
package so other people can use it.
The way you use these hooks is by adding the following to your top-level index.ts
:
import { WasmerSdk } from './hooks.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WasmerSdk log="info,wasmer_wasix=debug,wasmer_js=debug">
<App />
</WasmerSdk>
</React.StrictMode>,
)
The <WasmerSdk/>
component will load the SDK in the background.
The associated useWasmerSdk()
hook gives you a way to check the state of the loading and access the functionality it provides. You can also use the useWasmerPackage()
hook to load a package in the background.
function Terminal() {
const sdk = useWasmerSdk();
const pkg = useWasmerPackage("wasmer/python");
const [output, setOutput] = useState();
const [running, setRunning] = useState(false);
if (sdk.state == "loading") {
return <p>Loading...</p>;
}
if (pkg.state == "loaded" && !running) {
setRunning(true);
pkg.entrypoint.run()
.then(setOutput)
.finally(() => setRunning(false));
}
return <pre>{output.stdout}</pre>;
}
(code probably doesn't compile, but hopefully you get the gist)