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> {
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
View File

@@ -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();
});
}

View File

@@ -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()) {

View File

@@ -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);

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().
* 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);
}

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 {
if (typeof unknownData !== "string" && !(unknownData instanceof Uint8Array)) {
console.error(unknownData);