CODEBASE: Refactor and fix issues in db.ts (#2623)

This commit is contained in:
catloversg
2026-04-04 05:28:48 +07:00
committed by GitHub
parent 48fad72b6a
commit 2819947378
6 changed files with 100 additions and 36 deletions

View File

@@ -453,6 +453,9 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
async function loadGame(saveData: SaveData): Promise<boolean> { async function loadGame(saveData: SaveData): Promise<boolean> {
createScamUpdateText(); createScamUpdateText();
if (!saveData) { if (!saveData) {
console.error(
`Invalid save data. typeof saveData: ${typeof saveData}. saveData is an empty string: ${saveData === ""}`,
);
return false; return false;
} }
const jsonSaveString = await decodeSaveData(saveData); const jsonSaveString = await decodeSaveData(saveData);

101
src/db.ts
View File

@@ -1,4 +1,5 @@
import type { SaveData } from "./types"; import type { SaveData } from "./types";
import { isSaveData } from "./utils/TypeAssertion";
export class IndexedDBVersionError extends Error { export class IndexedDBVersionError extends Error {
constructor(message: string, options: ErrorOptions) { 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) => { return new Promise((resolve, reject) => {
if (!window.indexedDB) { if (!window.indexedDB) {
reject("Indexed DB does not exists"); reject(new Error("This browser does not support IndexedDB"));
} }
/** /**
* DB is called bitburnerSave * DB is called bitburnerSave
@@ -38,52 +39,96 @@ function getDB(): Promise<IDBObjectStore> {
if (this.error?.name === "VersionError") { if (this.error?.name === "VersionError") {
reject(new IndexedDBVersionError(this.error.message, { cause: this.error })); 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>) { indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) {
const db = this.result; const db = this.result;
// This should never happen unless the browser is buggy.
if (!db) { if (!db) {
reject(new Error("database loading result was undefined")); reject(new Error("Database opened successfully, but the result is somehow falsy."));
return; return;
} }
resolve(db.transaction(["savestring"], "readwrite").objectStore("savestring")); resolve(db);
}; };
}); });
} }
export function load(): Promise<SaveData> { /**
return getDB().then((db) => { * Load the save data from IndexedDB.
return new Promise<SaveData>((resolve, reject) => { *
const request = db.get("save") as IDBRequest<SaveData>; * Note that if skipCheckingLoadedData is true, there is no guarantee the resolved data is valid SaveData. Only pass
request.onerror = function (this: IDBRequest<SaveData>) { * true to skipCheckingLoadedData if you don't care if the loaded data is valid SaveData (e.g., download a backup of the
reject(new Error("Error in Database request to get save data", { cause: this.error })); * 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>) { const objectStore = transaction.objectStore("savestring");
resolve(this.result); 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> { export async function save(saveData: SaveData): Promise<void> {
return getDB().then((db) => { const db = await getDB();
return new Promise<void>((resolve, reject) => { return new Promise((resolve, reject) => {
// We'll save to IndexedDB const transaction = db.transaction(["savestring"], "readwrite");
const request = db.put(saveData, "save"); 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>) { const objectStore = transaction.objectStore("savestring");
reject(new Error("Error saving game to IndexedDB", { cause: this.error })); 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> { export function deleteGame(): Promise<void> {
return getDB().then((db) => { const request = window.indexedDB.deleteDatabase("bitburnerSave");
db.delete("save"); 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();
}); });
} }

View File

@@ -237,7 +237,7 @@ const Engine = {
} }
}, },
load: async function (saveData: SaveData) { load: async function (saveData?: SaveData) {
startExploits(); startExploits();
setupUncaughtPromiseHandler(); setupUncaughtPromiseHandler();
// Source files must be initialized early because save-game translation in // Source files must be initialized early because save-game translation in
@@ -245,7 +245,7 @@ const Engine = {
initSourceFiles(); initSourceFiles();
// Load game from save or create new game // Load game from save or create new game
if (await loadGame(saveData)) { if (saveData !== undefined && (await loadGame(saveData))) {
FormatsNeedToChange.emit(); FormatsNeedToChange.emit();
initBitNodeMultipliers(); initBitNodeMultipliers();
if (canAccessStockMarket()) { if (canAccessStockMarket()) {

View File

@@ -35,11 +35,15 @@ interface IProps {
} }
function exportSaveFile(): void { function exportSaveFile(): void {
load() load(true)
.then((content) => { .then((saveData) => {
const extension = isBinaryFormat(content) ? "json.gz" : "json"; 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}`; const filename = `RECOVERY_BITBURNER_${Date.now()}.${extension}`;
downloadContentAsFile(content, filename); downloadContentAsFile(saveData, filename);
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);

View File

@@ -537,8 +537,12 @@ Error: ${e}`,
* Backup pre-v3 save data. We must use the data in IndexedDB instead of calling saveObject.getSaveData(). * 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. * getSaveData() returns data in v3 format, so the exported data will not be importable in pre-v3.
*/ */
const saveData = await load(); const saveData = await load(true);
downloadContentAsFile(saveData, `bitburnerSave_backup_2.8.1_${Math.round(Player.lastUpdate / 1000)}.json.gz`); 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) { } catch (error) {
console.error("Cannot export pre-v3 save data", error); console.error("Cannot export pre-v3 save data", error);
} }

View File

@@ -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 { export function assertSaveData(unknownData: unknown): asserts unknownData is SaveData {
if (typeof unknownData !== "string" && !(unknownData instanceof Uint8Array)) { if (typeof unknownData !== "string" && !(unknownData instanceof Uint8Array)) {
console.error(unknownData); console.error(unknownData);