export { bootstrap }; /** * @typedef {Object} Reference * @property {Array} 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} */ 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} */ async function send_socket_msg(socket, msg) { // Send a request for all references socket.send(msg); // Wait for the response /** @type {Promise} */ 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} storeNames * @returns {Promise} */ 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} */ 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>} 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} 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; }