Skip to content

Commit bbfd875

Browse files
committed
chore: accounting for localstorage race conditions
1 parent 865a652 commit bbfd875

2 files changed

Lines changed: 31 additions & 19 deletions

File tree

packages/sdk/electron/__tests__/platform/ElectronStorage.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ it('handles error when clearing values', async () => {
9494
const storage = new ElectronStorage(logger);
9595
await storage.clear('key1');
9696

97-
expect(await storage.get('key1')).toEqual('value1');
97+
// Cache is updated synchronously — the in-memory state reflects the clear
98+
// even if the disk write failed. The data will be flushed on the next write.
99+
expect(await storage.get('key1')).toBeNull();
98100

99101
expect(logger.debug).not.toHaveBeenCalled();
100102
expect(logger.info).not.toHaveBeenCalled();
101103
expect(logger.warn).not.toHaveBeenCalled();
102-
expect(logger.error).toHaveBeenCalledWith(
103-
'Error clearing key from storage: key1, reason: write failed',
104-
);
104+
expect(logger.error).toHaveBeenCalledWith('Error flushing storage to disk: write failed');
105105
});
106106

107107
it('handles failed initialization when getting values', async () => {
@@ -225,14 +225,14 @@ it('handles error when setting values', async () => {
225225
const storage = new ElectronStorage(logger);
226226
await storage.set('key3', 'value3');
227227

228-
expect(await storage.get('key3')).toBeNull();
228+
// Cache is updated synchronously — the in-memory state reflects the set
229+
// even if the disk write failed. The data will be flushed on the next write.
230+
expect(await storage.get('key3')).toEqual('value3');
229231

230232
expect(logger.debug).not.toHaveBeenCalled();
231233
expect(logger.info).not.toHaveBeenCalled();
232234
expect(logger.warn).not.toHaveBeenCalled();
233-
expect(logger.error).toHaveBeenCalledWith(
234-
'Error setting key in storage: key3, reason: write failed',
235-
);
235+
expect(logger.error).toHaveBeenCalledWith('Error flushing storage to disk: write failed');
236236
});
237237

238238
it('getElectronStorage returns the same instance on subsequent calls', () => {

packages/sdk/electron/src/platform/ElectronStorage.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export default class ElectronStorage implements Storage {
99
private readonly _tempFile: string;
1010
private readonly _initialized: Promise<boolean>;
1111
private _cache: Map<string, string>;
12+
private _flushPending: boolean = false;
13+
private _flushQueue: Promise<void> = Promise.resolve();
1214

1315
constructor(private readonly _logger?: LDLogger) {
1416
this._storageFile = path.join(electron.app.getPath('userData'), 'ldcache');
@@ -58,7 +60,7 @@ export default class ElectronStorage implements Storage {
5860
} catch (cleanupError) {
5961
this._logError('Error cleaning up temporary file', cleanupError);
6062
}
61-
throw error; // Re-throw the original error
63+
throw error;
6264
}
6365
}
6466

@@ -78,11 +80,8 @@ export default class ElectronStorage implements Storage {
7880
try {
7981
await this._throwIfNotInitialized();
8082
if (this._cache.has(key)) {
81-
const cacheCopy = new Map(this._cache);
82-
cacheCopy.delete(key);
83-
await this._atomicWriteToFile(cacheCopy);
84-
// Only update cache if write was successful
85-
this._cache = cacheCopy;
83+
this._cache.delete(key);
84+
await this._scheduleFlush();
8685
}
8786
} catch (error) {
8887
this._logError(`Error clearing key from storage: ${key}, reason`, error);
@@ -103,15 +102,28 @@ export default class ElectronStorage implements Storage {
103102
async set(key: string, value: string): Promise<void> {
104103
try {
105104
await this._throwIfNotInitialized();
106-
const cacheCopy = new Map(this._cache);
107-
cacheCopy.set(key, value);
108-
await this._atomicWriteToFile(cacheCopy);
109-
// Only update cache if write was successful
110-
this._cache = cacheCopy;
105+
this._cache.set(key, value);
106+
await this._scheduleFlush();
111107
} catch (error) {
112108
this._logError(`Error setting key in storage: ${key}, reason`, error);
113109
}
114110
}
111+
112+
private _scheduleFlush(): Promise<void> {
113+
if (this._flushPending) {
114+
return this._flushQueue;
115+
}
116+
this._flushPending = true;
117+
this._flushQueue = this._flushQueue
118+
.then(async () => {
119+
this._flushPending = false;
120+
await this._atomicWriteToFile(new Map(this._cache));
121+
})
122+
.catch((error) => {
123+
this._logError('Error flushing storage to disk', error);
124+
});
125+
return this._flushQueue;
126+
}
115127
}
116128

117129
// Storage should be a singleton to support multiple instances of the SDK. This should have

0 commit comments

Comments
 (0)