Skip to content

Latest commit

 

History

History
286 lines (214 loc) · 7.07 KB

wasm.md

File metadata and controls

286 lines (214 loc) · 7.07 KB

WASM Setup

Building

Make sure you have all prerequisites installed.

You also need the wasm-pack tool from here.

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Development

Within the bcr-ebill-wasm crate, run these commands to build the project:

wasm-pack build --dev --target web

Release Build

wasm-pack build --target web

Running

After building the project for WASM, you'll find the WASM artifacts in the .crates/bcr-ebill-wasm/pkg folder including generated TypeScript bindings.

You can run this by serving it to the web, using any local HTTP-Server. For example, you can use http-server.

There are example index.html and main.js files, which provide a playground to test the created WASM artifacts.

Within the bcr-ebill-wasm crate, you can run:

http-server -c-1 .

This way, you can interact with the app at http://localhost:8080/.

The database used by Surreal is IndexedDb in the WASM version, so if you clear your IndexedDb (Dev tools -> Storage), you can reset it. Also, opening the app in another browser, or a private browser window, also starts with a blank slate.

API

The API can be used in the following way (you can also check more examples in main.js):

API Example

import * as wasm from '../pkg/bcr_ebill_wasm.js';

async function start() {
    let config = {
        bitcoin_network: "testnet",
        nostr_relay: "wss://bitcr-cloud-run-04-550030097098.europe-west1.run.app",
        surreal_db_connection: "indxdb://default",
        data_dir: ".",
        job_runner_initial_delay_seconds: 1,
        job_runner_check_interval_seconds: 600,
    };

    await wasm.default();
    await wasm.initialize_api(config);

    let notificationApi = wasm.Api.notification();
    let identityApi = wasm.Api.identity();
    let contactApi = wasm.Api.contact();
    let companyApi = wasm.Api.company();
    let billApi = wasm.Api.bill();
    let generalApi = wasm.Api.general();

    let identity;
    // Identity
    try {
        identity = await identityApi.detail();
        console.log("local identity:", identity);
    } catch(err) {
        console.log("No local identity found - creating..");
        await identityApi.create({
            name: "Johanna Smith",
            email: "[email protected]",
            postal_address: {
                country: "AT",
                city: "Vienna",
                zip: "1020",
                address: "street 1",
            }
        });
        identity = await identityApi.detail();
    }

    await notificationApi.subscribe((evt) => {
        console.log("Received event in JS: ", evt);
    });
}

await start();

TypeScript Bindings

We use Tsify for creating TypeScript bindings from Rust types. This is useful, as API consumers can just use those types to work with the library.

API functions

An exception are API functions, where we have to use #[wasm_bindgen(unchecked_param_type = "TypeName")] and #[wasm_bindgen(unchecked_return_type = "TypeName")] to document which types we accept and return.

The reason is, that we need to use serialized/deserialized wasm_bindgen::JsValue values to communicate with JS, so we need to annotate which types are actually behind these generic serialization types.

Example:

    #[wasm_bindgen(unchecked_return_type = "UploadFileResponse")]
    pub async fn upload(
        &self,
        #[wasm_bindgen(unchecked_param_type = "UploadFile")] payload: JsValue,
    ) -> Result<JsValue> {
        let upload_file: UploadFile = serde_wasm_bindgen::from_value(payload)?;
        let upload_file_handler: &dyn UploadFileHandler = &upload_file as &dyn UploadFileHandler;

        get_ctx()
            .file_upload_service
            .validate_attached_file(upload_file_handler)
            .await?;

        let file_upload_response = get_ctx()
            .file_upload_service
            .upload_file(upload_file_handler)
            .await?;

        let res = serde_wasm_bindgen::to_value(&file_upload_response.into_web())?;
        Ok(res)
    }

Which leads to

export class Bill {
  ...
  upload(payload: UploadFile): Promise<UploadFileResponse>;
  ...
}

export interface UploadFile {
    data: number[];
    extension: string | undefined;
    name: string;
}

export interface UploadFileResponse {
    file_upload_id: string;
}

Errors

Generally, most API functions are promise-based and return a Result<T, JSValue>, where the error type looks like this:

#[derive(Tsify, Debug, Clone, Serialize)]
#[tsify(into_wasm_abi)]
struct JsErrorData {
    error: &'static str,
    message: String,
    code: u16,
}

Which leads to

export interface JsErrorData {
    error: string;
    message: string;
    code: number;
}

On the JS side, it's enough to await the API functions and use try/catch for error-handling, or any other promise-based error-handling strategy.

Enums

There is another exception for enums, which should be represented as e.g. u8. For those, we just use #[wasm_bindgen].

Example:

#[wasm_bindgen]
#[repr(u8)]
#[derive(
    Debug, Copy, Clone, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, PartialEq, Eq,
)]
pub enum ContactTypeWeb {
    Person = 0,
    Company = 1,
}

Which leads to

export enum ContactTypeWeb {
  Person = 0,
  Company = 1,
}

If we would use tsify, it would automatically convert this to a string-based enum.

Example:

#[derive(Tsify, Debug, Copy, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub enum NotificationTypeWeb {
    General,
    Bill,
}

Which leads to

export type NotificationTypeWeb = "General" | "Bill";

Using the WASM API

The bcr-ebill-wasm API is published to this npm package.

You can simply add it to any JS/TS project:

  "dependencies": {
    "@bitcredit/bcr-ebill-wasm": "^0.3.0"
  }

If you use Vite, you'll have to configure the server to not optimize the WASM dependency:

import { defineConfig } from "vite";

export default defineConfig({
    optimizeDeps: {
        exclude: [
            "@bitcredit/bcr-ebill-wasm"
        ]
    }
});

With that, you can just import and use the WASM API in JS/TS:

import * as wasm from '@bitcredit/bcr-ebill-wasm';

async function start() {
    let config = {
        bitcoin_network: "testnet",
        nostr_relay: "wss://bitcr-cloud-run-04-550030097098.europe-west1.run.app",
        surreal_db_connection: "indxdb://default",
        data_dir: ".",
        job_runner_initial_delay_seconds: 1,
        job_runner_check_interval_seconds: 600,
    };
    await wasm.default();
    await wasm.initialize_api(config);

    let generalApi = wasm.Api.general();
    let currencies = await generalApi.currencies();
    console.log("currencies: ", currencies);
}
await start();