WebAssembly — Caching to HTML5 IndexedDB

C. Gerard Gallant
Jan 12, 2018 · 8 min read
// Request the wasm file from the server and compile it...   fetch(sWasmURI).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, g_importObject)
).then(results => {
// We've been working with the .instance object so far
objModuleInstance = results.instance;
// The results object also holds a .module object which is
// what we can cache:
// results.module
});
WebAssembly.instantiate(objModule, g_importObject).then(instance =>
g_objModuleInstance = instance
);
<!DOCTYPE html>   
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="button" value="Test" onclick="OnClickTest();" />
<script src="IndexedDB.js"></script>
<script type="text/javascript">
var g_importObject = {
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({ initial: 256 }),
'table': new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
}
};

// The WebAssembly module instance that we'll be working with
var g_objModuleInstance = null;

// If we need to change the structure of the database, we can
// increment the DB_VERSION value to trigger the
// onupgradeneeded event when opening the database
var DB_VERSION = 1;
var DB_NAME = "WasmCache";
var DB_OBJSTORE_MODULES = "Modules";

// We've set things up in such a way so that each wasm file
// can have a version and we only clear the items from the
// cache if the version doesn't match
var g_sTestWasmURI = "test.wasm";
var g_sTestWasmVersion = "1.0.0";

// Check to see if the module is cached and, if so, use that.
// Otherwise, download the module and cache it.
GetCompiledModuleFromIndexedDB(g_sTestWasmURI, g_sTestWasmVersion);


function GetCompiledModuleFromIndexedDB(sWasmURI, sWasmVersion) {
// If we successfully opened the database then...
OpenDB(DB_NAME, DB_VERSION, HandleUpgradeDB).then(dbConnection => {
// If we successfully obtained the requested record
// then...
GetRecordFromObjectStore(dbConnection, DB_OBJSTORE_MODULES, sWasmURI).then(objRecord => {
// If the version stored for this module doesn't
// match the version we need then the module cached
// is out of date...
if (objRecord.WasmVersion !== sWasmVersion) {
// Have the record deleted and then fetch the
// proper file
DeleteRecordFromObjectStore(dbConnection, DB_OBJSTORE_MODULES, sWasmURI).then(result => {
LoadWebAssemblyFromFile(dbConnection, sWasmURI, sWasmVersion);
}); }
else { // The cached record is the version we need...
// Have the module instantiated.
//
// NOTE: Unlike when we pass in the bytes to
// instantiate in the LoadWebAssemblyFromFile method
// below, we don't have a separate 'instance' and
// 'modules' object returned in this case since we
// started out with the module object. We're only
// passed back the instance in this case.
WebAssembly.instantiate(objRecord.WasmModule, g_importObject).then(instance =>
// Hold onto the module's instance so that we can
// reuse it
g_objModuleInstance = instance
);
} }, sErrorMsg => { // Error in GetRecordFromObjectStore... // We weren't able to pull the module from cache (most
// likely because it doesn't exist yet - hasn't been
// cached yet). Log the error and then fetch the file.
console.log(sErrorMsg);
LoadWebAssemblyFromFile(dbConnection, sWasmURI, sWasmVersion);
}); }, sErrorMsg => { // Error in OpenDB... // Log the error and then fetch the file (won't be able to
// cache it in this case because we don't have a database
// connection to work with)
console.log(sErrorMsg);
LoadWebAssemblyFromFile(null, sWasmURI, sWasmVersion);
}); }
// Called by indexeddb if the database was just created or if
// the database version was changed
function HandleUpgradeDB(evt) {
// Create the object store which will hold 3 properties:
// • WasmURI - (primary key) e.g. 'test.wasm'
// • WasmVersion - e.g. '1.0.1'
// • WasmModule - the compiled module
var dbConnection = evt.target.result;
dbConnection.createObjectStore(DB_OBJSTORE_MODULES, { keyPath: "WasmURI" });
}


function LoadWebAssemblyFromFile(dbConnection, sWasmURI, sWasmVersion) {
// Request the wasm file from the server and compile it...
fetch(sWasmURI).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, g_importObject)
).then(results => {
// Hold onto the module's instance so that we can reuse it
g_objModuleInstance = results.instance;

// Only do the following if we have a database connection
// object (this method will be passed a null if we failed
// to load the module from cache due to an error when
// trying to open the database)
if (dbConnection !== null) {
// WARNING: Not all browsers that support WebAssembly
// also support the ability to store the module in
// IndexedDB (seems to work fine in Edge 16 and in
// Firefox but it doesn't work for me in Chrome 63)
try {
// Create the object we're about to store
var objRecord = { "WasmURI": sWasmURI, "WasmVersion": sWasmVersion, "WasmModule": results.module };

// Cache the compiled module so that we don't have to
// pull the file from the server again unless we
// change the module's version number.
SaveRecordToObjectStore(dbConnection, DB_OBJSTORE_MODULES, objRecord);
}
catch (ex) {
console.log(`Unable to save the WebAssembly module to IndexedDB: ${ex.message}`);
}
}
}); }
function OnClickTest() {
// Call the module's add method and display the results
var iResult = g_objModuleInstance.exports._add(1, 2);
alert(iResult.toString());
}
</script>
</body>
</html>
// Helper methods to work with an IndexedDB database   
//
// Note: IndexedDB methods are asynchronous. To make things a bit
// easier to work with for the calling code, I've added Promises.

function OpenDB(sDatabaseName, sDatabaseVersion, fncUpgradeDB) {
return new Promise(function (fncResolve, fncReject) { // Make a request for the database to be opened
var dbRequest = indexedDB.open(sDatabaseName, sDatabaseVersion);

dbRequest.onerror = function (evt) {
fncReject(`Error in OpenDB: ${evt.target.error}`);
}

// Pass the database connection object to the resolve method
// of the promise
dbRequest.onsuccess = function (evt) {
fncResolve(evt.target.result);
}

// This event handler will only be called if we're creating
// the database for the first time or if we're upgrading the
// database to a new version (this will be triggered before
// the onsuccess event handler above if it does get called).
// Let the calling code handle upgrading the database if
// needed to keep this file as generic as possible.
dbRequest.onupgradeneeded = fncUpgradeDB;
});}


// Helper method to simplify the code some
function GetObjectStore(dbConnection, sObjectStoreName, sTransactionMode) {
// Create a transation and, from the transaction, get the object
// store object
return dbConnection.transaction([sObjectStoreName], sTransactionMode).objectStore(sObjectStoreName);
}


function GetRecordFromObjectStore(dbConnection, sObjectStoreName, sRecordID) {
return new Promise(function (fncResolve, fncReject) { // Request the record specified
var dbGetRequest = GetObjectStore(dbConnection, sObjectStoreName, "readonly").get(sRecordID);

dbGetRequest.onerror = function (evt) {
fncReject(`Error in GetRecordFromObjectStore: ${evt.target.error}`);
}

dbGetRequest.onsuccess = function (evt) {
// If we have a record then...(we have to check because there
// won't be a record if the database was just created)
var objRecord = evt.target.result;
if (objRecord) { fncResolve(objRecord); }
else { fncReject(`The record '${sRecordID}' was not found in the object store '${sObjectStoreName}'`); }
}
});}


function DeleteRecordFromObjectStore(dbConnection, sObjectStoreName, sRecordID) {
return new Promise(function (fncResolve, fncReject) { // Request the delete of the record specified
var dbDeleteRequest = GetObjectStore(dbConnection, sObjectStoreName, "readwrite").delete(sRecordID);

dbDeleteRequest.onerror = function (evt) {
fncReject(`Error in DeleteRecordFromObjectStore: ${evt.target.error}`);
}

dbDeleteRequest.onsuccess = function (evt) { fncResolve(); }
});}


function SaveRecordToObjectStore(dbConnection, sObjectStoreName, objRecord) {
// Request the put of our record (if it doesn't already exist,
// it gets added. otherwise, it gets updated)
var dbPutRequest = GetObjectStore(dbConnection, sObjectStoreName, "readwrite").put(objRecord);

dbPutRequest.onerror = function (evt) {
console.log(`Error in SaveToIndexedDB: ${evt.target.error}`);
}

dbPutRequest.onsuccess = function (evt) {
console.log(`Successfully stored the record`);
}
}
int add(int x, int y) { return x + y; }

C. Gerard Gallant

Written by

Senior Software Developer. Author of “WebAssembly in Action” with Manning Publications.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade