v86/src/browser/filestorage.js
Ernest Wong c2dcb440dd Filestorage: Refactor fallback and load-from-server
Fallback logic is moved to the caller's responsibility.

Empty FileStorages and FileStorages that load from the server are
separated into different classes. This is to avoid faults where the
caller of the constructor forgets to pass in a `baseurl` parameter and
leads to some confusing bug.
2020-08-30 19:37:15 -05:00

304 lines
8.6 KiB
JavaScript

"use strict";
const INDEXEDDB_STORAGE_VERSION = 1;
const INDEXEDDB_STORAGE_NAME = "IndexedDBFileStorage";
const INDEXEDDB_STORAGE_STORE = "Store";
const INDEXEDDB_STORAGE_KEYPATH = "sha256sum";
const INDEXEDDB_STORAGE_VALUEPATH = "data";
/** @interface */
function FileStorageInterface() {}
/**
* @param {string} sha256sum
* @return {!Promise<Uint8Array>}
*/
FileStorageInterface.prototype.get = function(sha256sum) {};
/**
* @param {string} sha256sum
* @param {!Uint8Array} data
* @return {!Promise}
*/
FileStorageInterface.prototype.set = function(sha256sum, buffer) {};
/**
* @constructor
* @implements {FileStorageInterface}
*/
function MemoryFileStorage()
{
/**
* From sha256sum to file data.
* @type {Map<string,Uint8Array>}
*/
this.filedata = new Map();
}
/**
* @param {string} sha256sum
* @return {Uint8Array}
*/
MemoryFileStorage.prototype.get = async function(sha256sum) // jshint ignore:line
{
dbg_assert(sha256sum !== "", "MemoryFileStorage get: sha256sum should not be an empty string");
return this.filedata.get(sha256sum);
}; // jshint ignore:line
/**
* @param {string} sha256sum
* @param {!Uint8Array} data
*/
MemoryFileStorage.prototype.set = async function(sha256sum, buffer) // jshint ignore:line
{
dbg_assert(sha256sum !== "", "MemoryFileStorage set: sha256sum should not be an empty string");
this.filedata.set(sha256sum, buffer);
}; // jshint ignore:line
/**
* @constructor
* @implements {FileStorageInterface}
*/
function IndexedDBFileStorage()
{
dbg_assert(window.indexedDB, "IndexedDBFileStorage - indexedDB not available.");
this.db = null;
}
IndexedDBFileStorage.try_create = async function() // jshint ignore:line
{
if(typeof window === "undefined" || !window.indexedDB)
{
throw new Error("IndexedDB is not available");
}
const file_storage = new IndexedDBFileStorage();
await file_storage.init(); // jshint ignore:line
return file_storage;
}; // jshint ignore:line
IndexedDBFileStorage.prototype.init = function()
{
dbg_assert(!this.db, "IndexedDBFileStorage init: Database already intiialised");
return new Promise((resolve, reject) =>
{
const open_request = indexedDB.open(INDEXEDDB_STORAGE_NAME, INDEXEDDB_STORAGE_VERSION);
open_request.onblocked = event =>
{
dbg_log("IndexedDB blocked by an older database version being opened.", LOG_9P);
};
open_request.onerror = event =>
{
dbg_log("Error opening IndexedDB! Are you in private browsing mode? Error:", LOG_9P);
dbg_log(open_request.error, LOG_9P);
reject();
};
open_request.onupgradeneeded = event =>
{
const db = open_request.result;
db.createObjectStore(INDEXEDDB_STORAGE_STORE, { keyPath: INDEXEDDB_STORAGE_KEYPATH });
};
open_request.onsuccess = event =>
{
this.db = open_request.result;
this.db.onabort = event =>
{
dbg_assert(false, "IndexedDBFileStorage: transaction aborted unexpectedly");
};
this.db.onclose = event =>
{
dbg_assert(false, "IndexedDBFileStorage: connection closed unexpectedly");
};
this.db.onerror = error =>
{
dbg_assert(false, "IndexedDBFileStorage: unexpected error: " + error);
};
this.db.onversionchange = event =>
{
// TODO: double check this message
dbg_log("Warning: another v86 instance is trying to open IndexedDB database but " +
"is blocked by this current v86 instance.", LOG_9P);
};
resolve();
};
});
};
/**
* @param {string} sha256sum
* @return {!Promise<Uint8Array>}
*/
IndexedDBFileStorage.prototype.get = function(sha256sum)
{
dbg_assert(this.db, "IndexedDBFileStorage get: Database is not initialised");
dbg_assert(sha256sum !== "", "IndexedDBFileStorage get: sha256sum should not be an empty string");
return new Promise((resolve, reject) =>
{
const transaction = this.db.transaction(INDEXEDDB_STORAGE_STORE, "readonly");
const store = transaction.objectStore(INDEXEDDB_STORAGE_STORE);
const request = store.get(sha256sum);
request.onsuccess = event =>
{
dbg_assert(!request.result || request.result.data instanceof Uint8Array,
"IndexedDBFileStorage get: invalid entry format: " + request.result);
if(request.result && request.result.data instanceof Uint8Array)
{
resolve(request.result.data);
}
else
{
resolve(null);
}
};
});
};
/**
* @param {string} sha256sum
* @param {!Uint8Array} data
* @return {!Promise}
*/
IndexedDBFileStorage.prototype.set = function(sha256sum, data)
{
dbg_assert(this.db, "IndexedDBFileStorage set: Database is not initialised");
dbg_assert(sha256sum !== "", "IndexedDBFileStorage set: sha256sum should not be an empty string");
return new Promise((resolve, reject) =>
{
const transaction = this.db.transaction(INDEXEDDB_STORAGE_STORE, "readwrite");
const store = transaction.objectStore(INDEXEDDB_STORAGE_STORE);
const request = store.put({
[INDEXEDDB_STORAGE_KEYPATH]: sha256sum,
[INDEXEDDB_STORAGE_VALUEPATH]: data,
});
request.onsuccess = event => resolve();
});
};
/**
* @constructor
* @implements {FileStorageInterface}
* @param {string} baseurl
*/
function ServerMemoryFileStorage(baseurl)
{
dbg_assert(baseurl, "ServerMemoryFileStorage: baseurl should not be empty");
this.empty_storage = new MemoryFileStorage();
this.baseurl = baseurl;
}
/**
* @private
* @param {string} sha256sum
* @return {!Promise<Uint8Array>}
*/
ServerMemoryFileStorage.prototype.load_from_server = function(sha256sum)
{
return new Promise((resolve, reject) =>
{
v86util.load_file(this.baseurl + sha256sum, { done: buffer =>
{
const data = new Uint8Array(buffer);
this.set(sha256sum, data).then(() => resolve(data));
}});
});
};
/**
* @param {string} sha256sum
* @return {Uint8Array}
*/
ServerMemoryFileStorage.prototype.get = async function(sha256sum) // jshint ignore:line
{
const data = await this.empty_storage.get(sha256sum); // jshint ignore:line
if(!data)
{
return await this.load_from_server(sha256sum); // jshint ignore:line
}
return data;
}; // jshint ignore:line
/**
* @param {string} sha256sum
* @param {!Uint8Array} data
*/
ServerMemoryFileStorage.prototype.set = async function(sha256sum, data) // jshint ignore:line
{
await this.empty_storage.set(sha256sum, data); // jshint ignore:line
}; // jshint ignore:line
/**
* @constructor
* @implements {FileStorageInterface}
* @param {string} baseurl
*/
function ServerIndexedDBFileStorage(baseurl)
{
dbg_assert(baseurl, "ServerIndexedDBFileStorage: baseurl should not be empty");
this.empty_storage = new IndexedDBFileStorage();
this.baseurl = baseurl;
}
/**
* @param {string} baseurl
* @return {IndexedDBFileStorage}
*/
ServerIndexedDBFileStorage.try_create = async function(baseurl) // jshint ignore:line
{
if(typeof window === "undefined" || !window.indexedDB)
{
throw new Error("IndexedDB is not available");
}
const file_storage = new ServerIndexedDBFileStorage(baseurl);
await file_storage.empty_storage.init(); // jshint ignore:line
return file_storage;
}; // jshint ignore:line
/**
* @private
* @param {string} sha256sum
* @return {!Promise<Uint8Array>}
*/
ServerIndexedDBFileStorage.prototype.load_from_server = function(sha256sum)
{
return new Promise((resolve, reject) =>
{
v86util.load_file(this.baseurl + sha256sum, { done: buffer =>
{
const data = new Uint8Array(buffer);
this.set(sha256sum, data).then(() => resolve(data));
}});
});
};
/**
* @param {string} sha256sum
* @return {Uint8Array}
*/
ServerIndexedDBFileStorage.prototype.get = async function(sha256sum) // jshint ignore:line
{
const data = await this.empty_storage.get(sha256sum); // jshint ignore:line
if(!data)
{
return await this.load_from_server(sha256sum); // jshint ignore:line
}
return data;
}; // jshint ignore:line
/**
* @param {string} sha256sum
* @param {!Uint8Array} data
*/
ServerIndexedDBFileStorage.prototype.set = async function(sha256sum, data) // jshint ignore:line
{
await this.empty_storage.set(sha256sum, data); // jshint ignore:line
}; // jshint ignore:line