Skip to content

Commit 24041fb

Browse files
committed
feat: IndexedDB support
- Modify StorageType to include IndexedDB - Use localforage for localStorage and IndexedDB operations - All functions are asynchronous due to this - A mock IndexedDB is provided in test and development mode PS: This change makes all storage operations asynchronous, which may require updates in parts of the codebase that use these operations.
1 parent 9fdb968 commit 24041fb

File tree

3 files changed

+5637
-3346
lines changed

3 files changed

+5637
-3346
lines changed

index.ts

Lines changed: 124 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,180 @@
1-
import { writable as internal, type Writable } from 'svelte/store'
2-
1+
import { get, writable as internal, type Writable } from "svelte/store";
2+
if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
3+
require("fake-indexeddb/auto");
4+
}
5+
import localforage from "localforage";
6+
declare type StoreDict<T> = { [key: string]: Persisted<T> };
37
declare type Updater<T> = (value: T) => T;
4-
declare type StoreDict<T> = { [key: string]: Persisted<T> }
58

69
interface Persisted<T> extends Writable<T> {
7-
reset: () => void
10+
set: (this: void, value: T) => Promise<void>;
11+
reset: () => Promise<void>;
12+
update: (callback: Updater<T>) => Promise<void>;
813
}
914

1015
/* eslint-disable @typescript-eslint/no-explicit-any */
1116
interface Stores {
12-
local: StoreDict<any>,
13-
session: StoreDict<any>,
17+
local: StoreDict<any>;
18+
session: StoreDict<any>;
19+
indexedDB: StoreDict<any>;
1420
}
1521

1622
const stores: Stores = {
1723
local: {},
18-
session: {}
19-
}
24+
session: {},
25+
indexedDB: {},
26+
};
2027

2128
export interface Serializer<T> {
22-
parse(text: string): T
23-
stringify(object: T): string
29+
parse(text: string): T;
30+
stringify(object: T): string;
2431
}
2532

26-
export type StorageType = 'local' | 'session'
33+
export type StorageType = "local" | "session" | "indexedDB";
2734

2835
export interface Options<StoreType, SerializerType> {
29-
serializer?: Serializer<SerializerType>
30-
storage?: StorageType,
31-
syncTabs?: boolean,
32-
onError?: (e: unknown) => void
33-
onWriteError?: (e: unknown) => void
34-
onParseError?: (newValue: string | null, e: unknown) => void
35-
beforeRead?: (val: SerializerType) => StoreType
36-
beforeWrite?: (val: StoreType) => SerializerType
36+
serializer?: Serializer<SerializerType>;
37+
storage?: StorageType;
38+
syncTabs?: boolean;
39+
onError?: (e: unknown) => void;
40+
onWriteError?: (e: unknown) => void;
41+
onParseError?: (newValue: string | null, e: unknown) => void;
42+
beforeRead?: (val: SerializerType) => StoreType;
43+
beforeWrite?: (val: StoreType) => SerializerType;
3744
}
3845

39-
function getStorage(type: StorageType) {
40-
return type === 'local' ? localStorage : sessionStorage
46+
async function getStorage(type: StorageType) {
47+
let storage: LocalForage | Storage | null;
48+
try {
49+
storage = type === "session" ? window.sessionStorage : localforage;
50+
if (type === "local") await storage.setDriver(localforage.LOCALSTORAGE);
51+
if (type === "indexedDB") await storage.setDriver(localforage.INDEXEDDB);
52+
} catch (error) {
53+
storage = null;
54+
}
55+
return storage;
4156
}
4257

4358
/** @deprecated `writable()` has been renamed to `persisted()` */
44-
export function writable<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Persisted<StoreType> {
45-
console.warn("writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'")
46-
return persisted<StoreType, SerializerType>(key, initialValue, options)
59+
export async function writable<StoreType, SerializerType = StoreType>(
60+
key: string,
61+
initialValue: StoreType,
62+
options?: Options<StoreType, SerializerType>
63+
): Promise<Persisted<StoreType>> {
64+
console.warn(
65+
"writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'"
66+
);
67+
return await persisted<StoreType, SerializerType>(key, initialValue, options);
4768
}
48-
export function persisted<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Persisted<StoreType> {
49-
if (options?.onError) console.warn("onError has been deprecated. Please use onWriteError instead")
50-
51-
const serializer = options?.serializer ?? JSON
52-
const storageType = options?.storage ?? 'local'
53-
const syncTabs = options?.syncTabs ?? true
54-
const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e))
55-
const onParseError = options?.onParseError ?? ((newVal, e) => console.error(`Error when parsing ${newVal ? '"' + newVal + '"' : "value"} from persisted store "${key}"`, e))
56-
57-
const beforeRead = options?.beforeRead ?? ((val) => val as unknown as StoreType)
58-
const beforeWrite = options?.beforeWrite ?? ((val) => val as unknown as SerializerType)
59-
60-
const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined'
61-
const storage = browser ? getStorage(storageType) : null
62-
63-
function updateStorage(key: string, value: StoreType) {
64-
const newVal = beforeWrite(value)
65-
69+
export async function persisted<StoreType, SerializerType = StoreType>(
70+
key: string,
71+
initialValue: StoreType,
72+
options?: Options<StoreType, SerializerType>
73+
): Promise<Persisted<StoreType>> {
74+
if (options?.onError)
75+
console.warn(
76+
"onError has been deprecated. Please use onWriteError instead"
77+
);
78+
79+
const serializer = options?.serializer ?? JSON;
80+
const storageType = options?.storage ?? "local";
81+
const syncTabs = options?.syncTabs ?? true;
82+
const onWriteError =
83+
options?.onWriteError ??
84+
options?.onError ??
85+
((e) =>
86+
console.error(
87+
`Error when writing value from persisted store "${key}" to ${storageType}`,
88+
e
89+
));
90+
const onParseError =
91+
options?.onParseError ??
92+
((newVal, e) =>
93+
console.error(
94+
`Error when parsing ${
95+
newVal ? '"' + newVal + '"' : "value"
96+
} from persisted store "${key}"`,
97+
e
98+
));
99+
100+
const beforeRead =
101+
options?.beforeRead ?? ((val) => val as unknown as StoreType);
102+
const beforeWrite =
103+
options?.beforeWrite ?? ((val) => val as unknown as SerializerType);
104+
105+
const browser =
106+
typeof window !== "undefined" && typeof document !== "undefined";
107+
const storage: Storage | LocalForage | null = browser
108+
? await getStorage(storageType)
109+
: null;
110+
async function updateStorage(key: string, value: StoreType) {
111+
const newVal = beforeWrite(value);
66112
try {
67-
storage?.setItem(key, serializer.stringify(newVal))
113+
await storage?.setItem(key, serializer.stringify(newVal));
68114
} catch (e) {
69-
onWriteError(e)
115+
onWriteError(e);
70116
}
71117
}
72118

73-
function maybeLoadInitial(): StoreType {
119+
async function maybeLoadInitial(): Promise<StoreType> {
74120
function serialize(json: any) {
75121
try {
76-
return <SerializerType>serializer.parse(json)
122+
return <SerializerType>serializer.parse(json);
77123
} catch (e) {
78-
onParseError(json, e)
124+
onParseError(json, e);
79125
}
80126
}
81-
const json = storage?.getItem(key)
82-
if (json == null) return initialValue
127+
const json = await storage?.getItem(key);
128+
if (json == null) return initialValue;
83129

84-
const serialized = serialize(json)
85-
if (serialized == null) return initialValue
130+
const serialized = serialize(json);
131+
if (serialized == null) return initialValue;
86132

87-
const newVal = beforeRead(serialized)
88-
return newVal
133+
const newVal = beforeRead(serialized);
134+
return newVal;
89135
}
90136

91137
if (!stores[storageType][key]) {
92-
const initial = maybeLoadInitial()
138+
const initial: StoreType = await maybeLoadInitial();
93139
const store = internal(initial, (set) => {
94-
if (browser && storageType == 'local' && syncTabs) {
95-
const handleStorage = (event: StorageEvent) => {
140+
if (browser && storageType == "local" && syncTabs) {
141+
const handleStorage = async (event: StorageEvent) => {
96142
if (event.key === key && event.newValue) {
97-
let newVal: any
143+
let newVal: any;
98144
try {
99-
newVal = serializer.parse(event.newValue)
145+
newVal = serializer.parse(event.newValue);
100146
} catch (e) {
101-
onParseError(event.newValue, e)
102-
return
147+
onParseError(event.newValue, e);
148+
return;
103149
}
104-
const processedVal = beforeRead(newVal)
150+
const processedVal = beforeRead(newVal);
105151

106-
set(processedVal)
152+
set(processedVal);
107153
}
108-
}
154+
};
109155

110-
window.addEventListener("storage", handleStorage)
156+
window.addEventListener("storage", handleStorage);
111157

112-
return () => window.removeEventListener("storage", handleStorage)
158+
return () => window.removeEventListener("storage", handleStorage);
113159
}
114-
})
160+
});
115161

116-
const { subscribe, set } = store
162+
const { subscribe, set } = store;
117163

118164
stores[storageType][key] = {
119-
set(value: StoreType) {
120-
set(value)
121-
updateStorage(key, value)
165+
async set(value: StoreType) {
166+
set(value);
167+
await updateStorage(key, value);
122168
},
123-
update(callback: Updater<StoreType>) {
124-
return store.update((last) => {
125-
const value = callback(last)
126-
127-
updateStorage(key, value)
128-
129-
return value
130-
})
169+
async update(callback: Updater<StoreType>) {
170+
// this is more concise than store.update
171+
await this.set(callback(get(store)));
131172
},
132-
reset() {
133-
this.set(initialValue)
173+
async reset() {
174+
await this.set(initialValue);
134175
},
135-
subscribe
136-
}
176+
subscribe,
177+
};
137178
}
138-
return stores[storageType][key]
179+
return stores[storageType][key];
139180
}

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,10 @@
5353
],
5454
"files": [
5555
"dist"
56-
]
56+
],
57+
"dependencies": {
58+
"@types/node": "^20.13.0",
59+
"fake-indexeddb": "^6.0.0",
60+
"localforage": "^1.10.0"
61+
}
5762
}

0 commit comments

Comments
 (0)