Creating Poll Canister Smart Contracts on the Internet Computer

How to repeatedly call a function on the IC with web workers without decreasing your dapp’s frontend performance.

​Web workers?

​If you are not familiar with web workers, you might ask yourself what they are. I advise that you review the MDN documentation for all of the details, but in few words, web workers are a simple way to run scripts in the background threads of the browser.

​Introduction

​The solution I aim to display defers the work from the UI to web workers. When the application starts, it initializes and starts the polling, which takes care of querying the IC repeatedly. Ultimately, to render the results in the UI, the backend threads transfer the data to the window of the browser.

  1. Use agent-js in the worker to get the identity.
  2. Call the IC.
  3. Transfer the results to the UI.

​1. Set up a web worker

For this tutorial, I will use vanilla JavaScript because when you create a new sample application with ​dfx​ or ​npm init ic​, the outcome is a barebones frontend app.

self.onmessage = async ($event) => {
console.log("Worker message", $event);
};
<main>
<button type="button">Sign in</button>

<textarea />
</main>
import { AuthClient } from "@dfinity/auth-client";

const signIn = async () => {
const authClient = await AuthClient.create();
await authClient.login({
onSuccess: async () => console.log(await authClient.isAuthenticated()),
onError: (err) => console.log(err),
identityProvider: `http://r7inp-6aaaa-aaaaa-aaabq-cai.localhost:8000?#authorize`,
});
};

const initSignInButton = () => {
const button = document.querySelector("button");
button.addEventListener("click", signIn, { passive: true });
};

const init = () => {
initSignInButton();
};

document.addEventListener("DOMContentLoaded", init);
const startWorker = () => {
const worker =
new Worker(new URL('./worker.js', import.meta.url));

worker.onmessage = ($event) => {
console.log("Window message", $event);
};

worker.postMessage({msg: 'start'});
};

const init = () => {
startWorker();
initSignInButton();
};

​2. Use agent-js in the worker to get the identity

I like to provide both “start” and “stop” options when I implement this type of cronjob. In this particular example we won’t use the “stop” feature, but it can be particularly useful when the worker is created from a component. It can be used to stop the timer when the component gets unmounted.

let timer;

self.onmessage = async ({ data }) => {
const { msg } = data;

switch (msg) {
case "start":
start();
break;
case stop:
stop();
}
};

const stop = () => clearInterval(timer);

const start = () => (timer = setInterval(call, 2000));

const call = async () => {
// TODO: call the IC
}
const call = async () => {
// Disable idle manager because web worker cannot access the window object of the UI
const authClient = await AuthClient.create({
idleOptions: {
disableIdle: true,
disableDefaultIdleCallback: true,
},
});

const isAuthenticated = await authClient.isAuthenticated();

if (!isAuthenticated) {
// User is not authenticated
return;
}

const identity = authClient.getIdentity();

// TODO: call the IC
};

​3. Call the IC

​Because of the same limitation as in the previous chapter, we have to provide a host​ information to create an actor that calls the backend. Moreover, because the declarations automatically generated by dfx contain a default actor that is only designed to work on the UI side, we have to copy the function that initializes an actor — and the canister ID constant — within our worker or in a dedicated file.

// Copy from auto generated declarations

const canisterId = process.env.ICWEBWORKER_BACKEND_CANISTER_ID;

export const createActor = (canisterId, options) => {
const agent = new HttpAgent(options ? { ...options.agentOptions } : {});

// Fetch root key for certificate validation during development
if (process.env.NODE_ENV !== "production") {
agent.fetchRootKey().catch((err) => {
console.warn(
"Unable to fetch root key. Check to ensure that your local replica is running"
);
console.error(err);
});
}

// Creates an actor with using the candid interface and the HttpAgent
return Actor.createActor(idlFactory, {
agent,
canisterId,
...(options ? options.actorOptions : {}),
});
};
const query = async ({ identity }) => {
const actor = createActor(canisterId, {
agentOptions: { identity,
host: `http://${canisterId}.localhost:8000/`
},
});
const greeting = await actor.greet();

// TODO: transfer results to UI
};
import Principal "mo:base/Principal";
import Nat "mo:base/Nat";

actor {
stable var counter = 0;

public shared({caller}) func greet() : async Text {
counter += 1;

return "Hello, " # Principal.toText(caller) # ". Counter: " # Nat.toText(counter);
};
};

​4. Transfer the results to the UI

​Now that we have obtained data from the IC, we can apply these to re-render the UI. Since the worker itself cannot do so, we transfer the data to the view with the help of postMessage.

const query = async ({ identity }) => {
const actor = createActor(canisterId, {
agentOptions: { identity,
host: `http://${canisterId}.localhost:8000/` },
});
const greeting = await actor.greet();

// Transfer data worker -> window / UI
postMessage({ msg: "result", greeting });
};
// In index.js - in the view
worker.onmessage = ({data}) => {
const {msg, greeting} = data;

switch (msg) {
case 'result':
document
.querySelector("textarea").value
+= `${greeting}\n`;
}
};

​Summary

​Thanks to the web worker, we can poll the Internet Computer without compromising on our dapp’s frontend performance. I ❤️ it.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store