Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-55] Create a React SDK #378

Open
Michael-F-Bryan opened this issue Dec 19, 2023 · 1 comment
Open

[SDK-55] Create a React SDK #378

Michael-F-Bryan opened this issue Dec 19, 2023 · 1 comment
Assignees
Labels
Feature linear Created by Linear-GitHub Sync

Comments

@Michael-F-Bryan
Copy link
Contributor

Michael-F-Bryan commented Dec 19, 2023

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.

SDK-55

@Michael-F-Bryan Michael-F-Bryan added linear Created by Linear-GitHub Sync Feature labels Dec 19, 2023
@Michael-F-Bryan Michael-F-Bryan self-assigned this Dec 19, 2023
@Michael-F-Bryan Michael-F-Bryan changed the title Create a React SDK [SDK-53] Create a React SDK Dec 19, 2023
@Michael-F-Bryan Michael-F-Bryan changed the title Create a React SDK [SDK-55] Create a React SDK Dec 19, 2023
@wasmerio wasmerio deleted a comment from linear bot Dec 19, 2023
@Michael-F-Bryan Michael-F-Bryan closed this as not planned Won't fix, can't repro, duplicate, stale Dec 19, 2023
@Michael-F-Bryan Michael-F-Bryan closed this as not planned Won't fix, can't repro, duplicate, stale Dec 21, 2023
@Michael-F-Bryan
Copy link
Contributor Author

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature linear Created by Linear-GitHub Sync
Projects
None yet
Development

No branches or pull requests

1 participant