Skip to content

Commit a96b1e3

Browse files
committed
Refactor storage internals into functional modules
1 parent 1f4dcce commit a96b1e3

12 files changed

Lines changed: 352 additions & 346 deletions

packages/js-toolkit/utils/storage/StorageStore.ts

Lines changed: 0 additions & 140 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { StorageProvider } from './types.js';
2+
import { getGlobalStorage } from './getGlobalStorage.js';
3+
import { getStorageKeys } from './getStorageKeys.js';
4+
5+
export function createLocalStorageProvider(): StorageProvider {
6+
return {
7+
syncEvent: 'storage',
8+
get storageArea() {
9+
return getGlobalStorage('localStorage');
10+
},
11+
get(key) {
12+
return getGlobalStorage('localStorage')?.getItem(key) ?? null;
13+
},
14+
set(key, value) {
15+
getGlobalStorage('localStorage')?.setItem(key, value);
16+
},
17+
remove(key) {
18+
getGlobalStorage('localStorage')?.removeItem(key);
19+
},
20+
has(key) {
21+
return getGlobalStorage('localStorage')?.getItem(key) !== null;
22+
},
23+
keys() {
24+
return getStorageKeys(getGlobalStorage('localStorage'));
25+
},
26+
clear() {
27+
getGlobalStorage('localStorage')?.clear();
28+
},
29+
};
30+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { StorageProvider } from './types.js';
2+
3+
export function createNoopProvider(): StorageProvider {
4+
return {
5+
get() {
6+
return null;
7+
},
8+
set() {},
9+
remove() {},
10+
has() {
11+
return false;
12+
},
13+
keys() {
14+
return [];
15+
},
16+
clear() {},
17+
};
18+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { StorageProvider } from './types.js';
2+
import { getGlobalStorage } from './getGlobalStorage.js';
3+
import { getStorageKeys } from './getStorageKeys.js';
4+
5+
export function createSessionStorageProvider(): StorageProvider {
6+
return {
7+
get storageArea() {
8+
return getGlobalStorage('sessionStorage');
9+
},
10+
get(key) {
11+
return getGlobalStorage('sessionStorage')?.getItem(key) ?? null;
12+
},
13+
set(key, value) {
14+
getGlobalStorage('sessionStorage')?.setItem(key, value);
15+
},
16+
remove(key) {
17+
getGlobalStorage('sessionStorage')?.removeItem(key);
18+
},
19+
has(key) {
20+
return getGlobalStorage('sessionStorage')?.getItem(key) !== null;
21+
},
22+
keys() {
23+
return getStorageKeys(getGlobalStorage('sessionStorage'));
24+
},
25+
clear() {
26+
getGlobalStorage('sessionStorage')?.clear();
27+
},
28+
};
29+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { hasWindow } from '../has.js';
2+
import { localStorageProvider } from './providers.js';
3+
import { jsonSerializer } from './serializers.js';
4+
import type { StorageInstance, StorageOptions } from './types.js';
5+
6+
export function createStorage<T extends Record<string, any> = Record<string, any>>(
7+
options: StorageOptions<T> = {},
8+
): StorageInstance<T> {
9+
const {
10+
provider = localStorageProvider,
11+
serializer = jsonSerializer,
12+
prefix = '',
13+
} = options;
14+
15+
const listeners = new Map<keyof T, Set<(value: any) => void>>();
16+
17+
function resolveKey(key: string): string {
18+
return `${prefix}${key}`;
19+
}
20+
21+
function syncHandler(event: Event): void {
22+
if (event instanceof StorageEvent) {
23+
if (provider.storageArea && event.storageArea !== provider.storageArea) {
24+
return;
25+
}
26+
27+
const rawKey = event.key;
28+
for (const [listenerKey, callbacks] of listeners) {
29+
if (resolveKey(listenerKey as string) === rawKey) {
30+
const newValue = event.newValue ? serializer.deserialize(event.newValue) : undefined;
31+
callbacks.forEach((callback) => callback(newValue));
32+
break;
33+
}
34+
}
35+
return;
36+
}
37+
38+
listeners.forEach((callbacks, key) => {
39+
const value = provider.get(resolveKey(key as string));
40+
const newValue = value !== null ? serializer.deserialize(value) : undefined;
41+
callbacks.forEach((callback) => callback(newValue));
42+
});
43+
}
44+
45+
if (hasWindow() && provider.syncEvent) {
46+
window.addEventListener(provider.syncEvent, syncHandler);
47+
}
48+
49+
return {
50+
get(key, defaultValue?) {
51+
const storedValue = provider.get(resolveKey(key as string));
52+
if (storedValue !== null) {
53+
const deserialized = serializer.deserialize(storedValue);
54+
if (deserialized !== undefined) {
55+
return deserialized;
56+
}
57+
}
58+
return defaultValue;
59+
},
60+
set(key, value) {
61+
provider.set(resolveKey(key as string), serializer.serialize(value));
62+
listeners.get(key)?.forEach((callback) => callback(value));
63+
},
64+
delete(key) {
65+
provider.remove(resolveKey(key as string));
66+
listeners.get(key)?.forEach((callback) => callback(undefined));
67+
},
68+
has(key) {
69+
return provider.has(resolveKey(key as string));
70+
},
71+
keys() {
72+
const allKeys = provider.keys();
73+
if (!prefix) {
74+
return allKeys as (keyof T)[];
75+
}
76+
77+
return allKeys
78+
.filter((key) => key.startsWith(prefix))
79+
.map((key) => key.slice(prefix.length) as keyof T);
80+
},
81+
clear() {
82+
if (prefix) {
83+
provider
84+
.keys()
85+
.filter((key) => key.startsWith(prefix))
86+
.forEach((key) => provider.remove(key));
87+
} else {
88+
provider.clear();
89+
}
90+
91+
listeners.forEach((callbacks) => {
92+
callbacks.forEach((callback) => callback(undefined));
93+
});
94+
},
95+
subscribe(key, callback) {
96+
if (!listeners.has(key)) {
97+
listeners.set(key, new Set());
98+
}
99+
100+
listeners.get(key)?.add(callback);
101+
102+
return () => {
103+
listeners.get(key)?.delete(callback);
104+
if (listeners.get(key)?.size === 0) {
105+
listeners.delete(key);
106+
}
107+
};
108+
},
109+
destroy() {
110+
listeners.clear();
111+
if (hasWindow() && provider.syncEvent) {
112+
window.removeEventListener(provider.syncEvent, syncHandler);
113+
}
114+
},
115+
};
116+
}

0 commit comments

Comments
 (0)