0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -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 * Save profiles on game close
* @returns IGameLogoutResponseData * @returns IGameLogoutResponseData
*/ */
public gameLogout( public async gameLogout(
url: string, url: string,
info: IEmptyRequestData, info: IEmptyRequestData,
sessionID: string, sessionID: string,
): IGetBodyResponseData<IGameLogoutResponseData> { ): Promise<IGetBodyResponseData<IGameLogoutResponseData>> {
this.saveServer.save(); await this.saveServer.saveProfile(sessionID);
return this.httpResponse.getBody({ status: "ok" }); return this.httpResponse.getBody({ status: "ok" });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ export class LauncherStaticRouter extends StaticRouter {
new RouteAction( new RouteAction(
"/launcher/profile/register", "/launcher/profile/register",
async (url: string, info: any, sessionID: string, output: string): Promise<string> => { 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( new RouteAction(
@ -57,7 +57,7 @@ export class LauncherStaticRouter extends StaticRouter {
new RouteAction( new RouteAction(
"/launcher/profile/remove", "/launcher/profile/remove",
async (url: string, info: any, sessionID: string, output: string): Promise<string> => { 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( 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 type { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer"; import { ConfigServer } from "@spt/servers/ConfigServer";
import { LocalisationService } from "@spt/services/LocalisationService"; 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 { HashUtil } from "@spt/utils/HashUtil";
import { JsonUtil } from "@spt/utils/JsonUtil"; import { JsonUtil } from "@spt/utils/JsonUtil";
import { Timer } from "@spt/utils/Timer"; import { Timer } from "@spt/utils/Timer";
@ -14,13 +14,12 @@ import { inject, injectAll, injectable } from "tsyringe";
@injectable() @injectable()
export class SaveServer { export class SaveServer {
protected profileFilepath = "user/profiles/"; protected profileFilepath = "user/profiles/";
protected profiles = {}; protected profiles: Map<string, ISptProfile> = new Map();
// onLoad = require("../bindings/SaveLoad"); protected onBeforeSaveCallbacks: Map<string, (profile: ISptProfile) => Promise<ISptProfile>> = new Map();
protected onBeforeSaveCallbacks = {}; protected saveSHA1: { [key: string]: string } = {};
protected saveMd5 = {};
constructor( constructor(
@inject("FileSystemSync") protected fileSystemSync: FileSystemSync, @inject("FileSystem") protected fileSystem: FileSystem,
@injectAll("SaveLoadRouter") protected saveLoadRouters: SaveLoadRouter[], @injectAll("SaveLoadRouter") protected saveLoadRouters: SaveLoadRouter[],
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HashUtil") protected hashUtil: HashUtil, @inject("HashUtil") protected hashUtil: HashUtil,
@ -34,8 +33,8 @@ export class SaveServer {
* @param id Id for save callback * @param id Id for save callback
* @param callback Callback to execute prior to running SaveServer.saveProfile() * @param callback Callback to execute prior to running SaveServer.saveProfile()
*/ */
public addBeforeSaveCallback(id: string, callback: (profile: Partial<ISptProfile>) => Partial<ISptProfile>): void { public addBeforeSaveCallback(id: string, callback: (profile: ISptProfile) => Promise<ISptProfile>): void {
this.onBeforeSaveCallbacks[id] = callback; this.onBeforeSaveCallbacks.set(id, callback);
} }
/** /**
@ -43,22 +42,23 @@ export class SaveServer {
* @param id Id of callback to remove * @param id Id of callback to remove
*/ */
public removeBeforeSaveCallback(id: string): void { public removeBeforeSaveCallback(id: string): void {
this.onBeforeSaveCallbacks[id] = undefined; this.onBeforeSaveCallbacks.delete(id);
} }
/** /**
* Load all profiles in /user/profiles folder into memory (this.profiles) * 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 { public async load(): Promise<void> {
this.fileSystemSync.ensureDir(this.profileFilepath); await this.fileSystem.ensureDir(this.profileFilepath);
// get files to load // 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 // load profiles
const timer = new Timer(); const timer = new Timer();
for (const file of files) { for (const file of files) {
this.loadProfile(FileSystemSync.getFileName(file)); await this.loadProfile(FileSystem.getFileName(file));
} }
this.logger.debug( this.logger.debug(
`Loading ${files.length} profile${files.length > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`, `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 * 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(); const timer = new Timer();
for (const sessionID in this.profiles) { 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( this.logger.debug(
`Saving ${profileCount} profile${profileCount > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`, `Saving ${profileCount} profile${profileCount > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`,
false, false,
@ -94,33 +95,35 @@ export class SaveServer {
throw new Error(`no profiles found in saveServer with id: ${sessionId}`); 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}`); throw new Error(`no profile found for sessionId: ${sessionId}`);
} }
return this.profiles[sessionId]; return profile;
} }
public profileExists(id: string): boolean { 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 * @returns Dictionary of ISptProfile
*/ */
public getProfiles(): Record<string, 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 * @param sessionID Id of profile to remove
* @returns true when deleted, false when profile not found * @returns true when deleted, false when profile not found
*/ */
public deleteProfileById(sessionID: string): boolean { public deleteProfileById(sessionID: string): boolean {
if (this.profiles[sessionID]) { if (this.profiles.get(sessionID)) {
delete this.profiles[sessionID]; this.profiles.delete(sessionID);
return true; return true;
} }
@ -132,11 +135,14 @@ export class SaveServer {
* @param profileInfo Basic profile data * @param profileInfo Basic profile data
*/ */
public createProfile(profileInfo: Info): void { 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}`); 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 * @param profileDetails Profile to save
*/ */
public addProfile(profileDetails: ISptProfile): void { 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 * Look up profile json in user/profiles by id and store in memory
* Execute saveLoadRouters callbacks after being loaded into memory * Execute saveLoadRouters callbacks after being loaded into memory
* @param sessionID Id of profile to store in 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 filename = `${sessionID}.json`;
const filePath = `${this.profileFilepath}${filename}`; const filePath = `${this.profileFilepath}${filename}`;
if (this.fileSystemSync.exists(filePath)) { if (await this.fileSystem.exists(filePath)) {
// File found, store in profiles[] // File found, store in profiles[]
this.profiles[sessionID] = this.fileSystemSync.readJson(filePath); this.profiles.set(sessionID, await this.fileSystem.readJson(filePath));
} }
// Run callbacks // Run callbacks
for (const callback of this.saveLoadRouters) { 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 * Save changes from in-memory profile to user/profiles json
* Execute onBeforeSaveCallbacks callbacks prior to being saved to json * Execute onBeforeSaveCallbacks callbacks prior to being saved to json
* @param sessionID profile id (user/profiles/id.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`; const filePath = `${this.profileFilepath}${sessionID}.json`;
// Run pre-save callbacks before we save into json // Run pre-save callbacks before we save into json
for (const callback in this.onBeforeSaveCallbacks) { for (const [id, callback] of this.onBeforeSaveCallbacks) {
const previous = this.profiles[sessionID]; 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 { 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) { } catch (error) {
this.logger.error(this.localisationService.getText("profile_save_callback_error", { callback, 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( const jsonProfile = this.jsonUtil.serialize(
this.profiles[sessionID], this.profiles.get(sessionID),
!this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).features.compressProfile, !this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE).features.compressProfile,
); );
const fmd5 = this.hashUtil.generateMd5ForData(jsonProfile); const sha1 = await this.hashUtil.generateSha1ForDataAsync(jsonProfile);
if (typeof this.saveMd5[sessionID] !== "string" || this.saveMd5[sessionID] !== fmd5) { if (typeof this.saveSHA1[sessionID] !== "string" || this.saveSHA1[sessionID] !== sha1) {
this.saveMd5[sessionID] = String(fmd5); this.saveSHA1[sessionID] = sha1;
// save profile to disk // save profile to disk
this.fileSystemSync.write(filePath, jsonProfile); await this.fileSystem.write(filePath, jsonProfile);
} }
} }
/** /**
* Remove a physical profile json from user/profiles * Remove a physical profile json from user/profiles
* @param sessionID Profile id to remove * @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`; 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, @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 account = this.saveServer.getProfile(sessionID).info;
const profileTemplateClone: ITemplateSide = this.cloner.clone( const profileTemplateClone: ITemplateSide = this.cloner.clone(
this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()], 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); this.saveServer.getProfile(sessionID).characters.scav = this.playerScavGenerator.generate(sessionID);
// Store minimal profile and reload it // Store minimal profile and reload it
this.saveServer.saveProfile(sessionID); await this.saveServer.saveProfile(sessionID);
this.saveServer.loadProfile(sessionID); await this.saveServer.loadProfile(sessionID);
// Completed account creation // Completed account creation
this.saveServer.getProfile(sessionID).info.wipe = false; this.saveServer.getProfile(sessionID).info.wipe = false;
this.saveServer.saveProfile(sessionID); await this.saveServer.saveProfile(sessionID);
return pmcData._id; return pmcData._id;
} }