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> {
|
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
101
src/db.ts
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user