@@ -9,13 +9,14 @@ import { FileSystem } from "@spt/utils/FileSystem";
9
9
import { HashUtil } from "@spt/utils/HashUtil" ;
10
10
import { JsonUtil } from "@spt/utils/JsonUtil" ;
11
11
import { Timer } from "@spt/utils/Timer" ;
12
+ import { Mutex } from "async-mutex" ;
12
13
import { inject , injectAll , injectable } from "tsyringe" ;
13
14
14
15
@injectable ( )
15
16
export class SaveServer {
16
17
protected profileFilepath = "user/profiles/" ;
17
18
protected profiles : Map < string , ISptProfile > = new Map ( ) ;
18
- protected profilesBeingSaved : Set < string > = new Set ( ) ;
19
+ protected profilesBeingSavedMutex : Map < string , Mutex > = new Map ( ) ;
19
20
protected onBeforeSaveCallbacks : Map < string , ( profile : ISptProfile ) => Promise < ISptProfile > > = new Map ( ) ;
20
21
protected saveSHA1 : { [ key : string ] : string } = { } ;
21
22
@@ -188,43 +189,48 @@ export class SaveServer {
188
189
* @returns A promise that resolves when saving is completed.
189
190
*/
190
191
public async saveProfile ( sessionID : string ) : Promise < void > {
191
- if ( ! this . profiles . get ( sessionID ) ) {
192
- throw new Error ( `Profile ${ sessionID } does not exist! Unable to save this profile!` ) ;
193
- }
194
-
195
- if ( this . profilesBeingSaved . has ( sessionID ) ) {
196
- throw new Error ( `Profile ${ sessionID } is already being saved!` ) ;
197
- }
192
+ // Get the current mutex if it exists, create a new one if it doesn't for this profile
193
+ const mutex = this . profilesBeingSavedMutex . get ( sessionID ) || new Mutex ( ) ;
194
+ this . profilesBeingSavedMutex . set ( sessionID , mutex ) ;
198
195
199
- this . profilesBeingSaved . add ( sessionID ) ;
196
+ const release = await mutex . acquire ( ) ;
200
197
201
- const filePath = `${ this . profileFilepath } ${ sessionID } .json` ;
198
+ try {
199
+ if ( ! this . profiles . get ( sessionID ) ) {
200
+ throw new Error ( `Profile ${ sessionID } does not exist! Unable to save this profile!` ) ;
201
+ }
202
202
203
- // Run pre-save callbacks before we save into json
204
- for ( const [ id , callback ] of this . onBeforeSaveCallbacks ) {
205
- const previous = this . profiles . get ( sessionID ) as ISptProfile ; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
206
- try {
207
- 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
208
- } catch ( error ) {
209
- this . logger . error ( this . localisationService . getText ( "profile_save_callback_error" , { callback, error } ) ) ;
210
- this . profiles . set ( sessionID , previous ) ;
203
+ const filePath = `${ this . profileFilepath } ${ sessionID } .json` ;
204
+
205
+ // Run pre-save callbacks before we save into json
206
+ for ( const [ id , callback ] of this . onBeforeSaveCallbacks ) {
207
+ const previous = this . profiles . get ( sessionID ) as ISptProfile ; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile
208
+ try {
209
+ 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
210
+ } catch ( error ) {
211
+ this . logger . error (
212
+ this . localisationService . getText ( "profile_save_callback_error" , { callback, error } ) ,
213
+ ) ;
214
+ this . profiles . set ( sessionID , previous ) ;
215
+ }
211
216
}
212
- }
213
217
214
- const jsonProfile = this . jsonUtil . serialize (
215
- this . profiles . get ( sessionID ) ,
216
- ! this . configServer . getConfig < ICoreConfig > ( ConfigTypes . CORE ) . features . compressProfile ,
217
- ) ;
218
+ const jsonProfile = this . jsonUtil . serialize (
219
+ this . profiles . get ( sessionID ) ,
220
+ ! this . configServer . getConfig < ICoreConfig > ( ConfigTypes . CORE ) . features . compressProfile ,
221
+ ) ;
218
222
219
- const sha1 = await this . hashUtil . generateSha1ForDataAsync ( jsonProfile ) ;
223
+ const sha1 = await this . hashUtil . generateSha1ForDataAsync ( jsonProfile ) ;
220
224
221
- if ( typeof this . saveSHA1 [ sessionID ] !== "string" || this . saveSHA1 [ sessionID ] !== sha1 ) {
222
- this . saveSHA1 [ sessionID ] = sha1 ;
223
- // save profile to disk
224
- await this . fileSystem . write ( filePath , jsonProfile ) ;
225
+ if ( typeof this . saveSHA1 [ sessionID ] !== "string" || this . saveSHA1 [ sessionID ] !== sha1 ) {
226
+ this . saveSHA1 [ sessionID ] = sha1 ;
227
+ // save profile to disk
228
+ await this . fileSystem . write ( filePath , jsonProfile ) ;
229
+ }
230
+ } finally {
231
+ // Release the current lock
232
+ release ( ) ;
225
233
}
226
-
227
- this . profilesBeingSaved . delete ( sessionID ) ;
228
234
}
229
235
230
236
/**
@@ -237,6 +243,16 @@ export class SaveServer {
237
243
238
244
this . profiles . delete ( sessionID ) ;
239
245
246
+ const pendingMutex = this . profilesBeingSavedMutex . get ( sessionID ) ;
247
+
248
+ if ( pendingMutex ) {
249
+ // If the profile already has a mutex assigned, cancel all pending locks and release.
250
+ pendingMutex . cancel ( ) ;
251
+ pendingMutex . release ( ) ;
252
+
253
+ this . profilesBeingSavedMutex . delete ( sessionID ) ;
254
+ }
255
+
240
256
await this . fileSystem . remove ( file ) ;
241
257
242
258
return ! ( await this . fileSystem . exists ( file ) ) ;
0 commit comments