0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 05:50:44 -05:00

Refactor SaveServer and the things that interact with it to be async (#1064)

Changes:

- Adds a map for `profiles`
- Changes `onBeforeSaveCallbacks` to be Promises
- Changes `SaveMD5` into `saveSHA1` as the async method for `saveSHA1`
isn't blocking
- Changes all routes and callbacks directly interacting with SaveServer
to be async

---------

Co-authored-by: Chomp <27521899+chompDev@users.noreply.github.com>
This commit is contained in:
Jesse 2025-01-10 10:15:14 +01:00 committed by GitHub
parent 9612ca2834
commit 1d1104a1e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 90 additions and 80 deletions

View File

@ -69,12 +69,12 @@ export class GameCallbacks implements OnLoad {
* Save profiles on game close
* @returns IGameLogoutResponseData
*/
public gameLogout(
public async gameLogout(
url: string,
info: IEmptyRequestData,
sessionID: string,
): IGetBodyResponseData<IGameLogoutResponseData> {
this.saveServer.save();
): Promise<IGetBodyResponseData<IGameLogoutResponseData>> {
await this.saveServer.saveProfile(sessionID);
return this.httpResponse.getBody({ status: "ok" });
}

View File

@ -27,8 +27,8 @@ export class LauncherCallbacks {
return !output ? "FAILED" : output;
}
public register(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK" {
const output = this.launcherController.register(info);
public async register(url: string, info: IRegisterData, sessionID: string): Promise<"FAILED" | "OK"> {
const output = await this.launcherController.register(info);
return !output ? "FAILED" : "OK";
}
@ -60,8 +60,8 @@ export class LauncherCallbacks {
return this.httpResponse.noBody("pong!");
}
public removeProfile(url: string, info: IRemoveProfileData, sessionID: string): string {
return this.httpResponse.noBody(this.saveServer.removeProfile(sessionID));
public async removeProfile(url: string, info: IRemoveProfileData, sessionID: string): Promise<string> {
return this.httpResponse.noBody(await this.saveServer.removeProfile(sessionID));
}
public getCompatibleTarkovVersion(): string {

View File

@ -33,13 +33,12 @@ export class ProfileCallbacks {
/**
* Handle client/game/profile/create
*/
public createProfile(
public async createProfile(
url: string,
info: IProfileCreateRequestData,
sessionID: string,
): IGetBodyResponseData<ICreateProfileResponse> {
const id = this.profileController.createProfile(info, sessionID);
return this.httpResponse.getBody({ uid: id });
): Promise<IGetBodyResponseData<ICreateProfileResponse>> {
return this.httpResponse.getBody({ uid: await this.profileController.createProfile(info, sessionID) });
}
/**

View File

@ -20,8 +20,8 @@ export class SaveCallbacks implements OnLoad, OnUpdate {
}
public async onLoad(): Promise<void> {
this.backupService.init();
this.saveServer.load();
await this.backupService.init();
await this.saveServer.load();
}
public getRoute(): string {
@ -31,7 +31,7 @@ export class SaveCallbacks implements OnLoad, OnUpdate {
public async onUpdate(secondsSinceLastRun: number): Promise<boolean> {
// run every 15 seconds
if (secondsSinceLastRun > this.coreConfig.profileSaveIntervalSeconds) {
this.saveServer.save();
await this.saveServer.save();
return true;
}
return false;

View File

@ -88,17 +88,17 @@ export class LauncherController {
return "";
}
public register(info: IRegisterData): string {
public async register(info: IRegisterData): Promise<string> {
for (const sessionID in this.saveServer.getProfiles()) {
if (info.username === this.saveServer.getProfile(sessionID).info.username) {
return "";
}
}
return this.createAccount(info);
return await this.createAccount(info);
}
protected createAccount(info: IRegisterData): string {
protected async createAccount(info: IRegisterData): Promise<string> {
const profileId = this.generateProfileId();
const scavId = this.generateProfileId();
const newProfileDetails: Info = {
@ -112,8 +112,8 @@ export class LauncherController {
};
this.saveServer.createProfile(newProfileDetails);
this.saveServer.loadProfile(profileId);
this.saveServer.saveProfile(profileId);
await this.saveServer.loadProfile(profileId);
await this.saveServer.saveProfile(profileId);
return profileId;
}

View File

@ -99,8 +99,8 @@ export class ProfileController {
* @param sessionID Player id
* @returns Profiles _id value
*/
public createProfile(info: IProfileCreateRequestData, sessionID: string): string {
return this.createProfileService.createProfile(sessionID, info);
public async createProfile(info: IProfileCreateRequestData, sessionID: string): Promise<string> {
return await this.createProfileService.createProfile(sessionID, info);
}
/**

View File

@ -75,7 +75,7 @@ export class ItemEventRouterDefinition extends Router {
}
export class SaveLoadRouter extends Router {
public handleLoad(profile: ISptProfile): ISptProfile {
public async handleLoad(profile: ISptProfile): Promise<ISptProfile> {
throw new Error("This method needs to be overrode by the router classes");
}
}

View File

@ -8,7 +8,7 @@ export class HealthSaveLoadRouter extends SaveLoadRouter {
return [new HandledRoute("spt-health", false)];
}
public override handleLoad(profile: ISptProfile): ISptProfile {
public override async handleLoad(profile: ISptProfile): Promise<ISptProfile> {
if (!profile.vitality) {
// Occurs on newly created profiles
profile.vitality = { health: undefined, effects: undefined };

View File

@ -8,7 +8,7 @@ export class InraidSaveLoadRouter extends SaveLoadRouter {
return [new HandledRoute("spt-inraid", false)];
}
public override handleLoad(profile: ISptProfile): ISptProfile {
public override async handleLoad(profile: ISptProfile): Promise<ISptProfile> {
if (profile.inraid === undefined) {
profile.inraid = { location: "none", character: "none" };
}

View File

@ -8,7 +8,7 @@ export class InsuranceSaveLoadRouter extends SaveLoadRouter {
return [new HandledRoute("spt-insurance", false)];
}
public override handleLoad(profile: ISptProfile): ISptProfile {
public override async handleLoad(profile: ISptProfile): Promise<ISptProfile> {
if (profile.insurance === undefined) {
profile.insurance = [];
}

View File

@ -9,7 +9,7 @@ export class ProfileSaveLoadRouter extends SaveLoadRouter {
return [new HandledRoute("spt-profile", false)];
}
public override handleLoad(profile: ISptProfile): ISptProfile {
public override async handleLoad(profile: ISptProfile): Promise<ISptProfile> {
if (!profile.characters) {
profile.characters = { pmc: {} as IPmcData, scav: {} as IPmcData };
}

View File

@ -92,7 +92,7 @@ export class GameStaticRouter extends StaticRouter {
sessionID: string,
output: string,
): Promise<IGetBodyResponseData<IGameLogoutResponseData>> => {
return this.gameCallbacks.gameLogout(url, info, sessionID);
return await this.gameCallbacks.gameLogout(url, info, sessionID);
},
),
new RouteAction(

View File

@ -27,7 +27,7 @@ export class LauncherStaticRouter extends StaticRouter {
new RouteAction(
"/launcher/profile/register",
async (url: string, info: any, sessionID: string, output: string): Promise<string> => {
return this.launcherCallbacks.register(url, info, sessionID);
return await this.launcherCallbacks.register(url, info, sessionID);
},
),
new RouteAction(
@ -57,7 +57,7 @@ export class LauncherStaticRouter extends StaticRouter {
new RouteAction(
"/launcher/profile/remove",
async (url: string, info: any, sessionID: string, output: string): Promise<string> => {
return this.launcherCallbacks.removeProfile(url, info, sessionID);
return await this.launcherCallbacks.removeProfile(url, info, sessionID);
},
),
new RouteAction(

View File

@ -5,7 +5,7 @@ import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { FileSystem } from "@spt/utils/FileSystem";
import { HashUtil } from "@spt/utils/HashUtil";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { Timer } from "@spt/utils/Timer";
@ -14,13 +14,12 @@ import { inject, injectAll, injectable } from "tsyringe";
@injectable()
export class SaveServer {
protected profileFilepath = "user/profiles/";
protected profiles = {};
// onLoad = require("../bindings/SaveLoad");
protected onBeforeSaveCallbacks = {};
protected saveMd5 = {};
protected profiles: Map<string, ISptProfile> = new Map();
protected onBeforeSaveCallbacks: Map<string, (profile: ISptProfile) => Promise<ISptProfile>> = new Map();
protected saveSHA1: { [key: string]: string } = {};
constructor(
@inject("FileSystemSync") protected fileSystemSync: FileSystemSync,
@inject("FileSystem") protected fileSystem: FileSystem,
@injectAll("SaveLoadRouter") protected saveLoadRouters: SaveLoadRouter[],
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -34,8 +33,8 @@ export class SaveServer {
* @param id Id for save callback
* @param callback Callback to execute prior to running SaveServer.saveProfile()
*/
public addBeforeSaveCallback(id: string, callback: (profile: Partial<ISptProfile>) => Partial<ISptProfile>): void {
this.onBeforeSaveCallbacks[id] = callback;
public addBeforeSaveCallback(id: string, callback: (profile: ISptProfile) => Promise<ISptProfile>): void {
this.onBeforeSaveCallbacks.set(id, callback);
}
/**
@ -43,22 +42,23 @@ export class SaveServer {
* @param id Id of callback to remove
*/
public removeBeforeSaveCallback(id: string): void {
this.onBeforeSaveCallbacks[id] = undefined;
this.onBeforeSaveCallbacks.delete(id);
}
/**
* Load all profiles in /user/profiles folder into memory (this.profiles)
* @returns A promise that resolves when loading all profiles is completed.
*/
public load(): void {
this.fileSystemSync.ensureDir(this.profileFilepath);
public async load(): Promise<void> {
await this.fileSystem.ensureDir(this.profileFilepath);
// get files to load
const files = this.fileSystemSync.getFiles(this.profileFilepath, false, ["json"]);
const files = await this.fileSystem.getFiles(this.profileFilepath, false, ["json"]);
// load profiles
const timer = new Timer();
for (const file of files) {
this.loadProfile(FileSystemSync.getFileName(file));
await this.loadProfile(FileSystem.getFileName(file));
}
this.logger.debug(
`Loading ${files.length} profile${files.length > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`,
@ -67,13 +67,14 @@ export class SaveServer {
/**
* Save changes for each profile from memory into user/profiles json
* @returns A promise that resolves when saving all profiles is completed.
*/
public save(): void {
public async save(): Promise<void> {
const timer = new Timer();
for (const sessionID in this.profiles) {
this.saveProfile(sessionID);
await this.saveProfile(sessionID);
}
const profileCount = Object.keys(this.profiles).length;
const profileCount = this.profiles.size;
this.logger.debug(
`Saving ${profileCount} profile${profileCount > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`,
false,
@ -94,33 +95,35 @@ export class SaveServer {
throw new Error(`no profiles found in saveServer with id: ${sessionId}`);
}
if (!this.profiles[sessionId]) {
const profile = this.profiles.get(sessionId);
if (!profile) {
throw new Error(`no profile found for sessionId: ${sessionId}`);
}
return this.profiles[sessionId];
return profile;
}
public profileExists(id: string): boolean {
return !!this.profiles[id];
return !!this.profiles.get(id);
}
/**
* Get all profiles from memory
* Gets all profiles from memory
* @returns Dictionary of ISptProfile
*/
public getProfiles(): Record<string, ISptProfile> {
return this.profiles;
return Object.fromEntries(this.profiles);
}
/**
* Delete a profile by id
* Delete a profile by id (Does not remove the profile file!)
* @param sessionID Id of profile to remove
* @returns true when deleted, false when profile not found
*/
public deleteProfileById(sessionID: string): boolean {
if (this.profiles[sessionID]) {
delete this.profiles[sessionID];
if (this.profiles.get(sessionID)) {
this.profiles.delete(sessionID);
return true;
}
@ -132,11 +135,14 @@ export class SaveServer {
* @param profileInfo Basic profile data
*/
public createProfile(profileInfo: Info): void {
if (this.profiles[profileInfo.id]) {
if (this.profiles.get(profileInfo.id)) {
throw new Error(`profile already exists for sessionId: ${profileInfo.id}`);
}
this.profiles[profileInfo.id] = { info: profileInfo, characters: { pmc: {}, scav: {} } };
this.profiles.set(profileInfo.id, {
info: profileInfo,
characters: { pmc: {}, scav: {} },
} as ISptProfile); // Cast to ISptProfile so the errors of having empty pmc and scav data disappear
}
/**
@ -144,25 +150,26 @@ export class SaveServer {
* @param profileDetails Profile to save
*/
public addProfile(profileDetails: ISptProfile): void {
this.profiles[profileDetails.info.id] = profileDetails;
this.profiles.set(profileDetails.info.id, profileDetails);
}
/**
* Look up profile json in user/profiles by id and store in memory
* Execute saveLoadRouters callbacks after being loaded into memory
* @param sessionID Id of profile to store in memory
* @returns A promise that resolves when loading is completed.
*/
public loadProfile(sessionID: string): void {
public async loadProfile(sessionID: string): Promise<void> {
const filename = `${sessionID}.json`;
const filePath = `${this.profileFilepath}${filename}`;
if (this.fileSystemSync.exists(filePath)) {
if (await this.fileSystem.exists(filePath)) {
// File found, store in profiles[]
this.profiles[sessionID] = this.fileSystemSync.readJson(filePath);
this.profiles.set(sessionID, await this.fileSystem.readJson(filePath));
}
// Run callbacks
for (const callback of this.saveLoadRouters) {
this.profiles[sessionID] = callback.handleLoad(this.getProfile(sessionID));
this.profiles.set(sessionID, await callback.handleLoad(this.getProfile(sessionID)));
}
}
@ -170,46 +177,50 @@ export class SaveServer {
* Save changes from in-memory profile to user/profiles json
* Execute onBeforeSaveCallbacks callbacks prior to being saved to json
* @param sessionID profile id (user/profiles/id.json)
* @returns void
* @returns A promise that resolves when saving is completed.
*/
public saveProfile(sessionID: string): void {
public async saveProfile(sessionID: string): Promise<void> {
if (!this.profiles.get(sessionID)) {
throw new Error(`Profile ${sessionID} does not exist! Unable to save this profile!`);
}
const filePath = `${this.profileFilepath}${sessionID}.json`;
// Run pre-save callbacks before we save into json
for (const callback in this.onBeforeSaveCallbacks) {
const previous = this.profiles[sessionID];
for (const [id, callback] of this.onBeforeSaveCallbacks) {
const previous = this.profiles.get(sessionID) as ISptProfile; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
try {
this.profiles[sessionID] = this.onBeforeSaveCallbacks[callback](this.profiles[sessionID]);
this.profiles.set(sessionID, await callback(this.profiles.get(sessionID) as ISptProfile)); // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
} catch (error) {
this.logger.error(this.localisationService.getText("profile_save_callback_error", { callback, error }));
this.profiles[sessionID] = previous;
this.profiles.set(sessionID, previous);
}
}
const jsonProfile = this.jsonUtil.serialize(
this.profiles[sessionID],
this.profiles.get(sessionID),
!this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).features.compressProfile,
);
const fmd5 = this.hashUtil.generateMd5ForData(jsonProfile);
if (typeof this.saveMd5[sessionID] !== "string" || this.saveMd5[sessionID] !== fmd5) {
this.saveMd5[sessionID] = String(fmd5);
const sha1 = await this.hashUtil.generateSha1ForDataAsync(jsonProfile);
if (typeof this.saveSHA1[sessionID] !== "string" || this.saveSHA1[sessionID] !== sha1) {
this.saveSHA1[sessionID] = sha1;
// save profile to disk
this.fileSystemSync.write(filePath, jsonProfile);
await this.fileSystem.write(filePath, jsonProfile);
}
}
/**
* Remove a physical profile json from user/profiles
* @param sessionID Profile id to remove
* @returns true if file no longer exists
* @returns A promise that is true if the file no longer exists
*/
public removeProfile(sessionID: string): boolean {
public async removeProfile(sessionID: string): Promise<boolean> {
const file = `${this.profileFilepath}${sessionID}.json`;
delete this.profiles[sessionID];
this.profiles.delete(sessionID);
this.fileSystemSync.remove(file);
await this.fileSystem.remove(file);
return !this.fileSystemSync.exists(file);
return !this.fileSystem.exists(file);
}
}

View File

@ -47,7 +47,7 @@ export class CreateProfileService {
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
) {}
public createProfile(sessionID: string, info: IProfileCreateRequestData): string {
public async createProfile(sessionID: string, info: IProfileCreateRequestData): Promise<string> {
const account = this.saveServer.getProfile(sessionID).info;
const profileTemplateClone: ITemplateSide = this.cloner.clone(
this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()],
@ -145,12 +145,12 @@ export class CreateProfileService {
this.saveServer.getProfile(sessionID).characters.scav = this.playerScavGenerator.generate(sessionID);
// Store minimal profile and reload it
this.saveServer.saveProfile(sessionID);
this.saveServer.loadProfile(sessionID);
await this.saveServer.saveProfile(sessionID);
await this.saveServer.loadProfile(sessionID);
// Completed account creation
this.saveServer.getProfile(sessionID).info.wipe = false;
this.saveServer.saveProfile(sessionID);
await this.saveServer.saveProfile(sessionID);
return pmcData._id;
}