Skip to content

Commit 4792391

Browse files
authored
feat: Add support for localStorage for the browser platform. (#566)
1 parent 3130c1c commit 4792391

File tree

5 files changed

+217
-1
lines changed

5 files changed

+217
-1
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import LocalStorage from '../../src/platform/LocalStorage';
2+
3+
it('can set values', async () => {
4+
const logger = {
5+
debug: jest.fn(),
6+
info: jest.fn(),
7+
warn: jest.fn(),
8+
error: jest.fn(),
9+
};
10+
// Storage here needs to be the global browser 'Storage' not the interface
11+
// for our platform.
12+
const spy = jest.spyOn(Storage.prototype, 'setItem');
13+
14+
const storage = new LocalStorage(logger);
15+
storage.set('test-key', 'test-value');
16+
expect(spy).toHaveBeenCalledWith('test-key', 'test-value');
17+
18+
expect(logger.debug).not.toHaveBeenCalled();
19+
expect(logger.info).not.toHaveBeenCalled();
20+
expect(logger.warn).not.toHaveBeenCalled();
21+
expect(logger.error).not.toHaveBeenCalled();
22+
});
23+
24+
it('can handle an error setting a value', async () => {
25+
const logger = {
26+
debug: jest.fn(),
27+
info: jest.fn(),
28+
warn: jest.fn(),
29+
error: jest.fn(),
30+
};
31+
// Storage here needs to be the global browser 'Storage' not the interface
32+
// for our platform.
33+
const spy = jest.spyOn(Storage.prototype, 'setItem');
34+
spy.mockImplementation(() => {
35+
throw new Error('bad');
36+
});
37+
38+
const storage = new LocalStorage(logger);
39+
storage.set('test-key', 'test-value');
40+
41+
expect(logger.debug).not.toHaveBeenCalled();
42+
expect(logger.info).not.toHaveBeenCalled();
43+
expect(logger.warn).not.toHaveBeenCalled();
44+
expect(logger.error).toHaveBeenCalledWith(
45+
'Error setting key in localStorage: test-key, reason: Error: bad',
46+
);
47+
});
48+
49+
it('can get values', async () => {
50+
const logger = {
51+
debug: jest.fn(),
52+
info: jest.fn(),
53+
warn: jest.fn(),
54+
error: jest.fn(),
55+
};
56+
// Storage here needs to be the global browser 'Storage' not the interface
57+
// for our platform.
58+
const spy = jest.spyOn(Storage.prototype, 'getItem');
59+
60+
const storage = new LocalStorage(logger);
61+
storage.get('test-key');
62+
expect(spy).toHaveBeenCalledWith('test-key');
63+
64+
expect(logger.debug).not.toHaveBeenCalled();
65+
expect(logger.info).not.toHaveBeenCalled();
66+
expect(logger.warn).not.toHaveBeenCalled();
67+
expect(logger.error).not.toHaveBeenCalled();
68+
});
69+
70+
it('can handle an error getting a value', async () => {
71+
const logger = {
72+
debug: jest.fn(),
73+
info: jest.fn(),
74+
warn: jest.fn(),
75+
error: jest.fn(),
76+
};
77+
// Storage here needs to be the global browser 'Storage' not the interface
78+
// for our platform.
79+
const spy = jest.spyOn(Storage.prototype, 'getItem');
80+
spy.mockImplementation(() => {
81+
throw new Error('bad');
82+
});
83+
84+
const storage = new LocalStorage(logger);
85+
storage.get('test-key');
86+
87+
expect(logger.debug).not.toHaveBeenCalled();
88+
expect(logger.info).not.toHaveBeenCalled();
89+
expect(logger.warn).not.toHaveBeenCalled();
90+
expect(logger.error).toHaveBeenCalledWith(
91+
'Error getting key from localStorage: test-key, reason: Error: bad',
92+
);
93+
});
94+
95+
it('can clear values', async () => {
96+
const logger = {
97+
debug: jest.fn(),
98+
info: jest.fn(),
99+
warn: jest.fn(),
100+
error: jest.fn(),
101+
};
102+
// Storage here needs to be the global browser 'Storage' not the interface
103+
// for our platform.
104+
const spy = jest.spyOn(Storage.prototype, 'removeItem');
105+
106+
const storage = new LocalStorage(logger);
107+
storage.clear('test-key');
108+
expect(spy).toHaveBeenCalledWith('test-key');
109+
110+
expect(logger.debug).not.toHaveBeenCalled();
111+
expect(logger.info).not.toHaveBeenCalled();
112+
expect(logger.warn).not.toHaveBeenCalled();
113+
expect(logger.error).not.toHaveBeenCalled();
114+
});
115+
116+
it('can handle an error clearing a value', async () => {
117+
const logger = {
118+
debug: jest.fn(),
119+
info: jest.fn(),
120+
warn: jest.fn(),
121+
error: jest.fn(),
122+
};
123+
// Storage here needs to be the global browser 'Storage' not the interface
124+
// for our platform.
125+
const spy = jest.spyOn(Storage.prototype, 'removeItem');
126+
spy.mockImplementation(() => {
127+
throw new Error('bad');
128+
});
129+
130+
const storage = new LocalStorage(logger);
131+
storage.clear('test-key');
132+
133+
expect(logger.debug).not.toHaveBeenCalled();
134+
expect(logger.info).not.toHaveBeenCalled();
135+
expect(logger.warn).not.toHaveBeenCalled();
136+
expect(logger.error).toHaveBeenCalledWith(
137+
'Error clearing key from localStorage: test-key, reason: Error: bad',
138+
);
139+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
export default {
3+
preset: 'ts-jest',
4+
testEnvironment: 'jest-environment-jsdom',
5+
transform: {
6+
"^.+\\.tsx?$": "ts-jest"
7+
// process `*.tsx` files with `ts-jest`
8+
},
9+
moduleNameMapper: {
10+
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__ mocks __/fileMock.js',
11+
},
12+
}

packages/sdk/browser/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
},
4040
"devDependencies": {
4141
"@launchdarkly/private-js-mocks": "0.0.1",
42-
"@testing-library/react": "^14.1.2",
4342
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
4443
"@types/jest": "^29.5.11",
4544
"@typescript-eslint/eslint-plugin": "^6.20.0",
@@ -52,9 +51,11 @@
5251
"eslint-plugin-jest": "^27.6.3",
5352
"eslint-plugin-prettier": "^5.0.0",
5453
"jest": "^29.7.0",
54+
"jest-environment-jsdom": "^29.7.0",
5555
"prettier": "^3.0.0",
5656
"rimraf": "^5.0.5",
5757
"ts-jest": "^29.1.1",
58+
"ts-node": "^10.9.2",
5859
"typedoc": "0.25.0",
5960
"typescript": "^5.5.3",
6061
"vite": "^5.4.1",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
LDOptions,
3+
Storage,
4+
/* platform */
5+
} from '@launchdarkly/js-client-sdk-common';
6+
7+
import LocalStorage, { isLocalStorageSupported } from './LocalStorage';
8+
9+
export default class BrowserPlatform /* implements platform.Platform */ {
10+
// encoding?: Encoding;
11+
// info: Info;
12+
// fileSystem?: Filesystem;
13+
// crypto: Crypto;
14+
// requests: Requests;
15+
storage?: Storage;
16+
17+
constructor(options: LDOptions) {
18+
if (isLocalStorageSupported()) {
19+
this.storage = new LocalStorage(options.logger);
20+
}
21+
}
22+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { LDLogger, Storage } from '@launchdarkly/js-client-sdk-common';
2+
3+
export function isLocalStorageSupported() {
4+
// Checking a symbol using typeof is safe, but directly accessing a symbol
5+
// which is not defined would be an error.
6+
return typeof localStorage !== 'undefined';
7+
}
8+
9+
/**
10+
* Implementation of Storage using localStorage for the browser.
11+
*
12+
* The Storage API is async, and localStorage is synchronous. This is fine,
13+
* and none of the methods need to internally await their operations.
14+
*/
15+
export default class PlatformStorage implements Storage {
16+
constructor(private readonly logger?: LDLogger) {}
17+
async clear(key: string): Promise<void> {
18+
try {
19+
localStorage.removeItem(key);
20+
} catch (error) {
21+
this.logger?.error(`Error clearing key from localStorage: ${key}, reason: ${error}`);
22+
}
23+
}
24+
25+
async get(key: string): Promise<string | null> {
26+
try {
27+
const value = localStorage.getItem(key);
28+
return value ?? null;
29+
} catch (error) {
30+
this.logger?.error(`Error getting key from localStorage: ${key}, reason: ${error}`);
31+
return null;
32+
}
33+
}
34+
35+
async set(key: string, value: string): Promise<void> {
36+
try {
37+
localStorage.setItem(key, value);
38+
} catch (error) {
39+
this.logger?.error(`Error setting key in localStorage: ${key}, reason: ${error}`);
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)