Skip to content

Commit 98d0ef7

Browse files
Merge branch 'inlocalstorage_storageAdapter' into inlocalstorage_sessionStorage
2 parents f1f5bf5 + a1c091b commit 98d0ef7

File tree

7 files changed

+124
-30
lines changed

7 files changed

+124
-30
lines changed

src/storages/inLocalStorage/MySegmentsCacheInLocal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
5151

5252
getRegisteredSegments(): string[] {
5353
const registeredSegments: string[] = [];
54-
for (let i = 0; i < this.storage.length; i++) {
54+
for (let i = 0, len = this.storage.length; i < len; i++) {
5555
const segmentName = this.keys.extractSegmentName(this.storage.key(i)!);
5656
if (segmentName) registeredSegments.push(segmentName);
5757
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { storageAdapter } from '../storageAdapter';
2+
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
3+
4+
5+
const syncWrapper = {
6+
getItem: jest.fn(() => JSON.stringify({ key1: 'value1' })),
7+
setItem: jest.fn(),
8+
removeItem: jest.fn(),
9+
};
10+
11+
const asyncWrapper = {
12+
getItem: jest.fn(() => Promise.resolve(JSON.stringify({ key1: 'value1' }))),
13+
setItem: jest.fn(() => Promise.resolve()),
14+
removeItem: jest.fn(() => Promise.resolve()),
15+
};
16+
17+
test.each([
18+
[syncWrapper],
19+
[asyncWrapper],
20+
])('storageAdapter', async (wrapper) => {
21+
22+
const storage = storageAdapter(loggerMock, 'prefix', wrapper);
23+
24+
expect(storage.length).toBe(0);
25+
26+
// Load cache from storage wrapper
27+
await storage.load();
28+
29+
expect(wrapper.getItem).toHaveBeenCalledWith('prefix');
30+
expect(storage.length).toBe(1);
31+
expect(storage.key(0)).toBe('key1');
32+
expect(storage.getItem('key1')).toBe('value1');
33+
34+
// Set item
35+
storage.setItem('key2', 'value2');
36+
expect(storage.getItem('key2')).toBe('value2');
37+
expect(storage.length).toBe(2);
38+
39+
// Remove item
40+
storage.removeItem('key1');
41+
expect(storage.getItem('key1')).toBe(null);
42+
expect(storage.length).toBe(1);
43+
44+
// Until a till key is set/removed, changes should not be saved/persisted
45+
await storage.whenSaved();
46+
expect(wrapper.setItem).not.toHaveBeenCalled();
47+
48+
// When setting a till key, changes should be saved/persisted immediately
49+
storage.setItem('.till', '1');
50+
expect(storage.length).toBe(2);
51+
expect(storage.key(0)).toBe('key2');
52+
expect(storage.key(1)).toBe('.till');
53+
54+
await storage.whenSaved();
55+
expect(wrapper.setItem).toHaveBeenCalledWith('prefix', JSON.stringify({ key2: 'value2', '.till': '1' }));
56+
57+
// When removing a till key, changes should be saved/persisted immediately
58+
storage.removeItem('.till');
59+
60+
await storage.whenSaved();
61+
expect(wrapper.setItem).toHaveBeenCalledWith('prefix', JSON.stringify({ key2: 'value2' }));
62+
63+
await storage.whenSaved();
64+
expect(wrapper.setItem).toHaveBeenCalledTimes(2);
65+
});

src/storages/inLocalStorage/__tests__/wrapper.mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
44

55
export const PREFIX = 'SPLITIO';
66

7-
export function createMemoryStorage(): SplitIO.StorageWrapper {
7+
export function createMemoryStorage(): SplitIO.AsyncStorageWrapper {
88
let cache: Record<string, string> = {};
99
return {
1010
getItem(key: string) {

src/storages/inLocalStorage/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ILogger } from '../../logger/types';
1919
import SplitIO from '../../../types/splitio';
2020
import { storageAdapter } from './storageAdapter';
2121

22-
function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): StorageAdapter | undefined {
22+
function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): StorageAdapter | undefined {
2323
if (wrapper) {
2424
if (isValidStorageWrapper(wrapper)) {
2525
return isWebStorage(wrapper) ?
@@ -72,7 +72,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
7272
},
7373

7474
destroy() {
75-
return storage.save && storage.save();
75+
return storage.whenSaved && storage.whenSaved();
7676
},
7777

7878
// When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).

src/storages/inLocalStorage/storageAdapter.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,58 @@ function isTillKey(key: string) {
77
return key.endsWith('.till');
88
}
99

10-
export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.StorageWrapper): StorageAdapter {
10+
export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): Required<StorageAdapter> {
11+
let keys: string[] = [];
1112
let cache: Record<string, string> = {};
1213

13-
let connectPromise: Promise<void> | undefined;
14-
let disconnectPromise = Promise.resolve();
14+
let loadPromise: Promise<void> | undefined;
15+
let savePromise = Promise.resolve();
16+
17+
function _save() {
18+
return savePromise = savePromise.then(() => {
19+
return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache)));
20+
}).catch((e) => {
21+
log.error(LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e);
22+
});
23+
}
1524

1625
return {
1726
load() {
18-
return connectPromise || (connectPromise = Promise.resolve(wrapper.getItem(prefix)).then((storedCache) => {
27+
return loadPromise || (loadPromise = Promise.resolve().then(() => {
28+
return wrapper.getItem(prefix);
29+
}).then((storedCache) => {
1930
cache = JSON.parse(storedCache || '{}');
31+
keys = Object.keys(cache);
2032
}).catch((e) => {
21-
log.error(LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
33+
log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
2234
}));
2335
},
24-
save() {
25-
return disconnectPromise = disconnectPromise.then(() => {
26-
return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache))).catch((e) => {
27-
log.error(LOG_PREFIX + 'Rejected promise calling storage setItem, with error: ' + e);
28-
});
29-
});
36+
whenSaved() {
37+
return savePromise;
3038
},
3139

3240
get length() {
33-
return Object.keys(cache).length;
41+
return keys.length;
3442
},
3543
getItem(key: string) {
3644
return cache[key] || null;
3745
},
3846
key(index: number) {
39-
return Object.keys(cache)[index] || null;
47+
return keys[index] || null;
4048
},
4149
removeItem(key: string) {
50+
const index = keys.indexOf(key);
51+
if (index === -1) return;
52+
keys.splice(index, 1);
4253
delete cache[key];
43-
if (isTillKey(key)) this.save!();
54+
55+
if (isTillKey(key)) _save();
4456
},
4557
setItem(key: string, value: string) {
58+
if (keys.indexOf(key) === -1) keys.push(key);
4659
cache[key] = value;
47-
if (isTillKey(key)) this.save!();
60+
61+
if (isTillKey(key)) _save();
4862
}
4963
};
5064
}

src/storages/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { ISettings } from '../types';
1111
export interface StorageAdapter {
1212
// Methods to support async storages
1313
load?: () => Promise<void>;
14-
save?: () => Promise<void>;
14+
whenSaved?: () => Promise<void>;
1515
// Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
1616
readonly length: number;
17-
getItem(key: string): string | null;
1817
key(index: number): string | null;
18+
getItem(key: string): string | null;
1919
removeItem(key: string): void;
2020
setItem(key: string, value: string): void;
2121
}

types/splitio.d.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,19 +458,34 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
458458
*/
459459
declare namespace SplitIO {
460460

461-
interface StorageWrapper {
461+
interface SyncStorageWrapper {
462462
/**
463-
* Returns a promise that resolves to the current value associated with the given key, or null if the given key does not exist.
463+
* Returns the value associated with the given key, or null if the key does not exist.
464464
*/
465-
getItem(key: string): Promise<string | null> | string | null;
465+
getItem(key: string): string | null;
466466
/**
467-
* Returns a promise that resolves when the value of the pair identified by key is set to value, creating a new key/value pair if none existed for key previously.
467+
* Sets the value for the given key, creating a new key/value pair if key does not exist.
468468
*/
469-
setItem(key: string, value: string): Promise<void> | void;
469+
setItem(key: string, value: string): void;
470470
/**
471-
* Returns a promise that resolves when the key/value pair with the given key is removed, if a key/value pair with the given key exists.
471+
* Removes the key/value pair for the given key, if the key exists.
472472
*/
473-
removeItem(key: string): Promise<void> | void;
473+
removeItem(key: string): void;
474+
}
475+
476+
interface AsyncStorageWrapper {
477+
/**
478+
* Returns a promise that resolves to the value associated with the given key, or null if the key does not exist.
479+
*/
480+
getItem(key: string): Promise<string | null>;
481+
/**
482+
* Returns a promise that resolves when the value of the pair identified by key is set to value, creating a new key/value pair if key does not exist.
483+
*/
484+
setItem(key: string, value: string): Promise<void>;
485+
/**
486+
* Returns a promise that resolves when the key/value pair for the given key is removed, if the key exists.
487+
*/
488+
removeItem(key: string): Promise<void>;
474489
}
475490

476491
/**
@@ -992,7 +1007,7 @@ declare namespace SplitIO {
9921007
*
9931008
* @defaultValue `window.localStorage`
9941009
*/
995-
wrapper?: StorageWrapper;
1010+
wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
9961011
}
9971012
/**
9981013
* Storage for asynchronous (consumer) SDK.
@@ -1338,7 +1353,7 @@ declare namespace SplitIO {
13381353
*
13391354
* @defaultValue `window.localStorage`
13401355
*/
1341-
wrapper?: StorageWrapper;
1356+
wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
13421357
};
13431358
}
13441359
/**

0 commit comments

Comments
 (0)