mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
CODEBASE: Refactor and fix issues in db.ts (#2623)
This commit is contained in:
@@ -453,6 +453,9 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
|
||||
async function loadGame(saveData: SaveData): Promise<boolean> {
|
||||
createScamUpdateText();
|
||||
if (!saveData) {
|
||||
console.error(
|
||||
`Invalid save data. typeof saveData: ${typeof saveData}. saveData is an empty string: ${saveData === ""}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const jsonSaveString = await decodeSaveData(saveData);
|
||||
|
||||
101
src/db.ts
101
src/db.ts
@@ -1,4 +1,5 @@
|
||||
import type { SaveData } from "./types";
|
||||
import { isSaveData } from "./utils/TypeAssertion";
|
||||
|
||||
export class IndexedDBVersionError extends Error {
|
||||
constructor(message: string, options: ErrorOptions) {
|
||||
@@ -7,10 +8,10 @@ export class IndexedDBVersionError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getDB(): Promise<IDBObjectStore> {
|
||||
function getDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
reject("Indexed DB does not exists");
|
||||
reject(new Error("This browser does not support IndexedDB"));
|
||||
}
|
||||
/**
|
||||
* DB is called bitburnerSave
|
||||
@@ -38,52 +39,96 @@ function getDB(): Promise<IDBObjectStore> {
|
||||
if (this.error?.name === "VersionError") {
|
||||
reject(new IndexedDBVersionError(this.error.message, { cause: this.error }));
|
||||
}
|
||||
reject(this.error ?? new Error("Failed to get IDB"));
|
||||
reject(this.error ?? new Error("Cannot open a connection to IndexedDB"));
|
||||
};
|
||||
indexedDbRequest.onblocked = function (this: IDBRequest<IDBDatabase>) {
|
||||
reject(
|
||||
new Error("Database in use by another tab. Please close all other Bitburner tabs.", { cause: this.error }),
|
||||
);
|
||||
};
|
||||
|
||||
indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) {
|
||||
const db = this.result;
|
||||
// This should never happen unless the browser is buggy.
|
||||
if (!db) {
|
||||
reject(new Error("database loading result was undefined"));
|
||||
reject(new Error("Database opened successfully, but the result is somehow falsy."));
|
||||
return;
|
||||
}
|
||||
resolve(db.transaction(["savestring"], "readwrite").objectStore("savestring"));
|
||||
resolve(db);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function load(): Promise<SaveData> {
|
||||
return getDB().then((db) => {
|
||||
return new Promise<SaveData>((resolve, reject) => {
|
||||
const request = db.get("save") as IDBRequest<SaveData>;
|
||||
request.onerror = function (this: IDBRequest<SaveData>) {
|
||||
reject(new Error("Error in Database request to get save data", { cause: this.error }));
|
||||
};
|
||||
/**
|
||||
* Load the save data from IndexedDB.
|
||||
*
|
||||
* Note that if skipCheckingLoadedData is true, there is no guarantee the resolved data is valid SaveData. Only pass
|
||||
* true to skipCheckingLoadedData if you don't care if the loaded data is valid SaveData (e.g., download a backup of the
|
||||
* save data, regardless of what it is).
|
||||
*
|
||||
* @param skipCheckingLoadedData
|
||||
* @returns
|
||||
*/
|
||||
export async function load(skipCheckingLoadedData = false): Promise<SaveData | undefined> {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(["savestring"], "readonly");
|
||||
const rejectHandler = () => {
|
||||
reject(transaction.error ?? new Error("Cannot load game data. Transaction aborted or encountered an error."));
|
||||
};
|
||||
transaction.onerror = rejectHandler;
|
||||
transaction.onabort = rejectHandler;
|
||||
|
||||
request.onsuccess = function (this: IDBRequest<SaveData>) {
|
||||
resolve(this.result);
|
||||
};
|
||||
});
|
||||
const objectStore = transaction.objectStore("savestring");
|
||||
const request = objectStore.get("save");
|
||||
|
||||
request.onsuccess = function () {
|
||||
const result: unknown = request.result;
|
||||
// In some cases, we don't care if the loaded data is valid SaveData.
|
||||
if (skipCheckingLoadedData) {
|
||||
resolve(result as SaveData | undefined);
|
||||
return;
|
||||
}
|
||||
if (result !== undefined && !isSaveData(result)) {
|
||||
console.error("Invalid save data in IndexedDB:", result);
|
||||
reject(new Error("Save data exists, but its type is invalid."));
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function save(saveData: SaveData): Promise<void> {
|
||||
return getDB().then((db) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// We'll save to IndexedDB
|
||||
const request = db.put(saveData, "save");
|
||||
export async function save(saveData: SaveData): Promise<void> {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(["savestring"], "readwrite");
|
||||
const rejectHandler = () => {
|
||||
reject(transaction.error ?? new Error("Cannot save game data. Transaction aborted or encountered an error."));
|
||||
};
|
||||
transaction.onerror = rejectHandler;
|
||||
transaction.onabort = rejectHandler;
|
||||
|
||||
request.onerror = function (this: IDBRequest<IDBValidKey>) {
|
||||
reject(new Error("Error saving game to IndexedDB", { cause: this.error }));
|
||||
};
|
||||
const objectStore = transaction.objectStore("savestring");
|
||||
objectStore.put(saveData, "save");
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
// transaction.oncomplete is used instead of request.onsuccess to ensure durability.
|
||||
// A request can succeed in memory, but the transaction may still fail to commit due to disk I/O errors, quota
|
||||
// limits, or system interruptions.
|
||||
transaction.oncomplete = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteGame(): Promise<void> {
|
||||
return getDB().then((db) => {
|
||||
db.delete("save");
|
||||
const request = window.indexedDB.deleteDatabase("bitburnerSave");
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = function () {
|
||||
reject(request.error ?? new Error("Cannot delete save data"));
|
||||
};
|
||||
request.onblocked = function () {
|
||||
reject(new Error("Database in use by another tab. Please close all other Bitburner tabs."));
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ const Engine = {
|
||||
}
|
||||
},
|
||||
|
||||
load: async function (saveData: SaveData) {
|
||||
load: async function (saveData?: SaveData) {
|
||||
startExploits();
|
||||
setupUncaughtPromiseHandler();
|
||||
// Source files must be initialized early because save-game translation in
|
||||
@@ -245,7 +245,7 @@ const Engine = {
|
||||
initSourceFiles();
|
||||
// Load game from save or create new game
|
||||
|
||||
if (await loadGame(saveData)) {
|
||||
if (saveData !== undefined && (await loadGame(saveData))) {
|
||||
FormatsNeedToChange.emit();
|
||||
initBitNodeMultipliers();
|
||||
if (canAccessStockMarket()) {
|
||||
|
||||
@@ -35,11 +35,15 @@ interface IProps {
|
||||
}
|
||||
|
||||
function exportSaveFile(): void {
|
||||
load()
|
||||
.then((content) => {
|
||||
const extension = isBinaryFormat(content) ? "json.gz" : "json";
|
||||
load(true)
|
||||
.then((saveData) => {
|
||||
if (saveData === undefined) {
|
||||
console.error("There is no save data, but the recovery mode was activated.");
|
||||
return;
|
||||
}
|
||||
const extension = isBinaryFormat(saveData) ? "json.gz" : "json";
|
||||
const filename = `RECOVERY_BITBURNER_${Date.now()}.${extension}`;
|
||||
downloadContentAsFile(content, filename);
|
||||
downloadContentAsFile(saveData, filename);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -537,8 +537,12 @@ Error: ${e}`,
|
||||
* Backup pre-v3 save data. We must use the data in IndexedDB instead of calling saveObject.getSaveData().
|
||||
* getSaveData() returns data in v3 format, so the exported data will not be importable in pre-v3.
|
||||
*/
|
||||
const saveData = await load();
|
||||
downloadContentAsFile(saveData, `bitburnerSave_backup_2.8.1_${Math.round(Player.lastUpdate / 1000)}.json.gz`);
|
||||
const saveData = await load(true);
|
||||
if (saveData !== undefined) {
|
||||
downloadContentAsFile(saveData, `bitburnerSave_backup_2.8.1_${Math.round(Player.lastUpdate / 1000)}.json.gz`);
|
||||
} else {
|
||||
console.error("Cannot back up save data before migrating to v3. The save data is somehow undefined.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cannot export pre-v3 save data", error);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,14 @@ export function assertNumberArray(unknownData: unknown, assertFinite = false): a
|
||||
}
|
||||
}
|
||||
|
||||
export function isSaveData(unknownData: unknown): unknownData is SaveData {
|
||||
if (typeof unknownData === "string") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return unknownData instanceof Uint8Array && unknownData.buffer instanceof ArrayBuffer;
|
||||
}
|
||||
|
||||
export function assertSaveData(unknownData: unknown): asserts unknownData is SaveData {
|
||||
if (typeof unknownData !== "string" && !(unknownData instanceof Uint8Array)) {
|
||||
console.error(unknownData);
|
||||
|
||||
Reference in New Issue
Block a user