offline-web/exp2/static/client.js

198 lines
5.9 KiB
JavaScript

export { bootstrap };
/**
* @typedef {Object} Reference
* @property {Array<Reference>} dependents
* @property {string} path
* @property {string} object_id
* @property {string} content_address
*/
/**
* @typedef {Object} ServerMsg
* @property {Reference?} Reference
* @property {string?} Object
*/
/**
* @param {WebSocket} socket
* @returns {Promise<ServerMsg>}
*/
async function load_bootstrap(socket) {
// Wait for the connection to be established
const data = await send_socket_msg(socket,
JSON.stringify("Bootstrap"));
return data;
}
/**
* @param {WebSocket} socket
* @param {string} msg
* @returns {Promise<ServerMsg>}
*/
async function send_socket_msg(socket, msg) {
// Send a request for all references
socket.send(msg);
// Wait for the response
/** @type {Promise<String>} */
const stream = await new Promise((resolve, reject) => {
socket.onmessage = (event) => {
resolve(event.data.text());
};
socket.onerror = (_error) => reject(new Error("WebSocket error occurred"));
});
let data = await stream;
return JSON.parse(data);
}
/**
* @param {String} dbName
* @param {Array<String>} storeNames
* @returns {Promise<IDBDatabase>}
*/
async function openDatabase(dbName, storeNames) {
return await new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
for (var storeName of storeNames) {
// Create the object store if it doesn't exist
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
}
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
/**
* Stores a reference object in the IndexedDB.
* @param {IDBObjectStore} store
* @param {Object} reference
* @param {string} root_path
* @returns {Promise<any>}
*/
function storeObject(store, reference, root_path) {
return new Promise((resolve, reject) => {
const request = store.put(JSON.stringify(reference), root_path);
request.onerror = (evt) => {
reject(evt.target.error);
console.log("Failed to store object", evt);
};
request.onsuccess = (evt) => {
resolve(evt.target.result);
};
});
}
/**
* @param {IDBObjectStore} refStore
* @param {Object} reference
* @returns {Promise<Array<Reference>>} An array of references
*/
function load_reference_paths(refStore, reference) {
return new Promise(async (resolve, _reject) => {
let references = [];
references.push(reference);
if (reference.dependents) {
for (var dep of reference.dependents) {
references = references.concat(await load_reference_paths(refStore, dep));
}
}
await storeObject(refStore, reference, reference.path);
resolve(references);
});
}
/**
* @param {WebSocket} socket
* @param {IDBDatabase} db
* @param {string} storeName
* @param {Array<Reference>} references
*/
async function load_objects_and_store(socket, db, references, storeName) {
let objects = []
for (var ref of references) {
/** @type {Response} */
if (ref.dependents && ref.dependents.length != 0) {
continue; // not a leaf object
}
let data = await send_socket_msg(socket, JSON.stringify({ "GetObject": ref.content_address }));
if (!data.Object) {
throw { error: "Not an object" };
}
objects.push({ id: ref.content_address, content: data.Object });
}
const objectTrxAndStore = await getStoreAndTransaction(db, storeName);
for (var obj of objects) {
await storeObject(objectTrxAndStore.store, obj.content, obj.id);
}
await new Promise((resolve, reject) => {
objectTrxAndStore.trx.oncomplete = () => resolve();
objectTrxAndStore.trx.onerror = (event) => reject(event.target.error);
});
}
/**
* @param {string} storeName
* @param {IDBDatabase} db
* @returns {Promise<{trx: IDBTransaction, store: IDBObjectStore}>} The transaction and object store.
*/
async function getStoreAndTransaction(db, storeName) {
const transaction = db.transaction([storeName], "readwrite");
return { trx: transaction, store: transaction.objectStore(storeName) };
}
/**
* @returns {Number} The number of milliseconds it took to bootstrap.
*/
async function bootstrap() {
const refStoreName = "references";
const objectStoreName = "objects";
const databaseName = "MerkleStore";
const start = new Date().getTime();
const socket = new WebSocket(`ws://${window.location.host}/api/v1/ws`);
await new Promise((resolve, reject) => {
socket.onopen = () => resolve();
socket.onerror = (error) => reject(new Error("WebSocket connection failed" + error));
});
const data = await load_bootstrap(socket);
if (!data.Reference) {
throw { error: "Not a Reference" };
}
const db = await openDatabase(databaseName, [refStoreName, objectStoreName]);
const refTrxAndStore = await getStoreAndTransaction(db, refStoreName);
// Use a promise to wait for the transaction to complete
const transactionComplete = new Promise((resolve, reject) => {
refTrxAndStore.trx.oncomplete = () => resolve();
refTrxAndStore.trx.onerror = (event) => reject(event.target.error);
});
const refs = await load_reference_paths(refTrxAndStore.store, data.Reference);
// Wait for the transaction to complete
await transactionComplete;
await load_objects_and_store(socket, db, refs, objectStoreName);
const end = new Date().getTime();
return end - start;
}