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:
parent
9612ca2834
commit
1d1104a1e8
@ -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" });
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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" };
|
||||
}
|
||||
|
@ -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 = [];
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user