Skip to content

Commit 4411c5d

Browse files
Implement basic storageAdapter to support async storages with getItem and setItem methods
1 parent 358a6b7 commit 4411c5d

File tree

6 files changed

+83
-58
lines changed

6 files changed

+83
-58
lines changed

src/storages/inLocalStorage/MySegmentsCacheInLocal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
import { StorageAdapter } from '.';
12
import { ILogger } from '../../logger/types';
23
import { isNaNNumber } from '../../utils/lang';
34
import { AbstractMySegmentsCacheSync } from '../AbstractMySegmentsCacheSync';
45
import type { MySegmentsKeyBuilder } from '../KeyBuilderCS';
56
import { LOG_PREFIX, DEFINED } from './constants';
6-
import SplitIO from '../../../types/splitio';
77

88
export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
99

1010
private readonly keys: MySegmentsKeyBuilder;
1111
private readonly log: ILogger;
12-
private readonly localStorage: SplitIO.Storage;
12+
private readonly localStorage: StorageAdapter;
1313

14-
constructor(log: ILogger, keys: MySegmentsKeyBuilder, localStorage: SplitIO.Storage) {
14+
constructor(log: ILogger, keys: MySegmentsKeyBuilder, localStorage: StorageAdapter) {
1515
super();
1616
this.log = log;
1717
this.keys = keys;

src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { usesSegments } from '../AbstractSplitsCacheSync';
77
import { KeyBuilderCS } from '../KeyBuilderCS';
88
import { IRBSegmentsCacheSync } from '../types';
99
import { LOG_PREFIX } from './constants';
10-
import SplitIO from '../../../types/splitio';
10+
import { StorageAdapter } from '.';
1111

1212
export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
1313

1414
private readonly keys: KeyBuilderCS;
1515
private readonly log: ILogger;
16-
private readonly localStorage: SplitIO.Storage;
16+
private readonly localStorage: StorageAdapter;
1717

18-
constructor(settings: ISettings, keys: KeyBuilderCS, localStorage: SplitIO.Storage) {
18+
constructor(settings: ISettings, keys: KeyBuilderCS, localStorage: StorageAdapter) {
1919
this.keys = keys;
2020
this.log = settings.log;
2121
this.localStorage = localStorage;

src/storages/inLocalStorage/SplitsCacheInLocal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ILogger } from '../../logger/types';
66
import { LOG_PREFIX } from './constants';
77
import { ISettings } from '../../types';
88
import { setToArray } from '../../utils/lang/sets';
9-
import SplitIO from '../../../types/splitio';
9+
import { StorageAdapter } from '.';
1010

1111
/**
1212
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
@@ -17,9 +17,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
1717
private readonly log: ILogger;
1818
private readonly flagSetsFilter: string[];
1919
private hasSync?: boolean;
20-
private readonly localStorage: SplitIO.Storage;
20+
private readonly localStorage: StorageAdapter;
2121

22-
constructor(settings: ISettings, keys: KeyBuilderCS, localStorage: SplitIO.Storage) {
22+
constructor(settings: ISettings, keys: KeyBuilderCS, localStorage: StorageAdapter) {
2323
super();
2424
this.keys = keys;
2525
this.log = settings.log;

src/storages/inLocalStorage/index.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,67 @@ import { validateCache } from './validateCache';
1818
import { ILogger } from '../../logger/types';
1919
import SplitIO from '../../../types/splitio';
2020

21-
function validateStorage(log: ILogger, storage?: SplitIO.Storage) {
21+
export interface StorageAdapter {
22+
// Methods to support async storages
23+
load?: () => Promise<void>;
24+
save?: () => Promise<void>;
25+
// Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
26+
readonly length: number;
27+
getItem(key: string): string | null;
28+
key(index: number): string | null;
29+
removeItem(key: string): void;
30+
setItem(key: string, value: string): void;
31+
}
32+
33+
function isTillKey(key: string) {
34+
return key.endsWith('.till');
35+
}
36+
37+
function storageAdapter(log: ILogger, prefix: string, storage: SplitIO.Storage): StorageAdapter {
38+
let cache: Record<string, string> = {};
39+
40+
let connectPromise: Promise<void> | undefined;
41+
let disconnectPromise = Promise.resolve();
42+
43+
return {
44+
load() {
45+
return connectPromise || (connectPromise = storage.getItem(prefix).then((storedCache) => {
46+
cache = JSON.parse(storedCache || '{}');
47+
}).catch((e) => {
48+
log.error(LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
49+
}));
50+
},
51+
save() {
52+
return disconnectPromise = disconnectPromise.then(() => {
53+
return storage.setItem(prefix, JSON.stringify(cache)).catch((e) => {
54+
log.error(LOG_PREFIX + 'Rejected promise calling storage setItem, with error: ' + e);
55+
});
56+
});
57+
},
58+
59+
get length() {
60+
return Object.keys(cache).length;
61+
},
62+
getItem(key: string) {
63+
return cache[key] || null;
64+
},
65+
key(index: number) {
66+
return Object.keys(cache)[index] || null;
67+
},
68+
removeItem(key: string) {
69+
delete cache[key];
70+
if (isTillKey(key)) this.save!();
71+
},
72+
setItem(key: string, value: string) {
73+
cache[key] = value;
74+
if (isTillKey(key)) this.save!();
75+
}
76+
};
77+
}
78+
79+
function validateStorage(log: ILogger, prefix: string, storage?: SplitIO.Storage): StorageAdapter | undefined {
2280
if (storage) {
23-
if (isStorageValid(storage)) return storage;
81+
if (isStorageValid(storage)) return storageAdapter(log, prefix, storage);
2482
log.warn(LOG_PREFIX + 'Invalid storage provided. Falling back to LocalStorage API');
2583
}
2684

@@ -39,7 +97,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
3997
function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
4098
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
4199

42-
const storage = validateStorage(log, options.storage);
100+
const storage = validateStorage(log, prefix, options.storage);
43101
if (!storage) return InMemoryStorageCSFactory(params);
44102

45103
const matchingKey = getMatching(settings.core.key);
@@ -67,8 +125,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
67125
},
68126

69127
destroy() {
70-
// @TODO return `storageWrapper.disconnect()`
71-
return Promise.resolve();
128+
return storage.save && storage.save();
72129
},
73130

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

src/storages/inLocalStorage/validateCache.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
77
import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
88
import { KeyBuilderCS } from '../KeyBuilderCS';
99
import SplitIO from '../../../types/splitio';
10+
import { StorageAdapter } from '.';
1011

1112
const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10;
1213
const MILLIS_IN_A_DAY = 86400000;
@@ -16,7 +17,7 @@ const MILLIS_IN_A_DAY = 86400000;
1617
*
1718
* @returns `true` if cache should be cleared, `false` otherwise
1819
*/
19-
function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: SplitIO.Storage, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
20+
function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
2021
const { log } = settings;
2122

2223
// Check expiration
@@ -67,9 +68,9 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Spl
6768
*
6869
* @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
6970
*/
70-
export function validateCache(options: SplitIO.InLocalStorageOptions, storage: SplitIO.Storage, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
71+
export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
7172

72-
return Promise.resolve().then(() => {
73+
return Promise.resolve(storage.load && storage.load()).then(() => {
7374
const currentTimestamp = Date.now();
7475
const isThereCache = splits.getChangeNumber() > -1;
7576

types/splitio.d.ts

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -451,50 +451,17 @@ declare namespace SplitIO {
451451

452452
interface Storage {
453453
/**
454-
* Returns the number of key/value pairs.
455-
*
456-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/length)
457-
*/
458-
readonly length: number;
459-
/**
460-
* Removes all key/value pairs, if there are any.
461-
*
462-
* Dispatches a storage event on Window objects holding an equivalent Storage object.
463-
*
464-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/clear)
465-
*/
466-
clear(): void;
467-
/**
468-
* Returns the current value associated with the given key, or null if the given key does not exist.
469-
*
470-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)
471-
*/
472-
getItem(key: string): string | null;
473-
/**
474-
* Returns the name of the nth key, or null if n is greater than or equal to the number of key/value pairs.
475-
*
476-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/key)
454+
* Returns a promise that resolves to the current value associated with the given key, or null if the given key does not exist.
477455
*/
478-
key(index: number): string | null;
456+
getItem(key: string): Promise<string | null>;
479457
/**
480-
* Removes the key/value pair with the given key, if a key/value pair with the given key exists.
481-
*
482-
* Dispatches a storage event on Window objects holding an equivalent Storage object.
483-
*
484-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/removeItem)
485-
*/
486-
removeItem(key: string): void;
487-
/**
488-
* Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
489-
*
490-
* Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.)
491-
*
492-
* Dispatches a storage event on Window objects holding an equivalent Storage object.
493-
*
494-
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)
458+
* 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.
495459
*/
496-
setItem(key: string, value: string): void;
497-
[name: string]: any;
460+
setItem(key: string, value: string): Promise<void>;
461+
// /**
462+
// * 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.
463+
// */
464+
// removeItem(key: string): Promise<void>;
498465
}
499466

500467
/**

0 commit comments

Comments
 (0)