|
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> }; |
3 | 7 | declare type Updater<T> = (value: T) => T; |
4 | | -declare type StoreDict<T> = { [key: string]: Persisted<T> } |
5 | 8 |
|
6 | 9 | 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>; |
8 | 13 | } |
9 | 14 |
|
10 | 15 | /* eslint-disable @typescript-eslint/no-explicit-any */ |
11 | 16 | interface Stores { |
12 | | - local: StoreDict<any>, |
13 | | - session: StoreDict<any>, |
| 17 | + local: StoreDict<any>; |
| 18 | + session: StoreDict<any>; |
| 19 | + indexedDB: StoreDict<any>; |
14 | 20 | } |
15 | 21 |
|
16 | 22 | const stores: Stores = { |
17 | 23 | local: {}, |
18 | | - session: {} |
19 | | -} |
| 24 | + session: {}, |
| 25 | + indexedDB: {}, |
| 26 | +}; |
20 | 27 |
|
21 | 28 | export interface Serializer<T> { |
22 | | - parse(text: string): T |
23 | | - stringify(object: T): string |
| 29 | + parse(text: string): T; |
| 30 | + stringify(object: T): string; |
24 | 31 | } |
25 | 32 |
|
26 | | -export type StorageType = 'local' | 'session' |
| 33 | +export type StorageType = "local" | "session" | "indexedDB"; |
27 | 34 |
|
28 | 35 | 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; |
37 | 44 | } |
38 | 45 |
|
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; |
41 | 56 | } |
42 | 57 |
|
43 | 58 | /** @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); |
47 | 68 | } |
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); |
66 | 112 | try { |
67 | | - storage?.setItem(key, serializer.stringify(newVal)) |
| 113 | + await storage?.setItem(key, serializer.stringify(newVal)); |
68 | 114 | } catch (e) { |
69 | | - onWriteError(e) |
| 115 | + onWriteError(e); |
70 | 116 | } |
71 | 117 | } |
72 | 118 |
|
73 | | - function maybeLoadInitial(): StoreType { |
| 119 | + async function maybeLoadInitial(): Promise<StoreType> { |
74 | 120 | function serialize(json: any) { |
75 | 121 | try { |
76 | | - return <SerializerType>serializer.parse(json) |
| 122 | + return <SerializerType>serializer.parse(json); |
77 | 123 | } catch (e) { |
78 | | - onParseError(json, e) |
| 124 | + onParseError(json, e); |
79 | 125 | } |
80 | 126 | } |
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; |
83 | 129 |
|
84 | | - const serialized = serialize(json) |
85 | | - if (serialized == null) return initialValue |
| 130 | + const serialized = serialize(json); |
| 131 | + if (serialized == null) return initialValue; |
86 | 132 |
|
87 | | - const newVal = beforeRead(serialized) |
88 | | - return newVal |
| 133 | + const newVal = beforeRead(serialized); |
| 134 | + return newVal; |
89 | 135 | } |
90 | 136 |
|
91 | 137 | if (!stores[storageType][key]) { |
92 | | - const initial = maybeLoadInitial() |
| 138 | + const initial: StoreType = await maybeLoadInitial(); |
93 | 139 | 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) => { |
96 | 142 | if (event.key === key && event.newValue) { |
97 | | - let newVal: any |
| 143 | + let newVal: any; |
98 | 144 | try { |
99 | | - newVal = serializer.parse(event.newValue) |
| 145 | + newVal = serializer.parse(event.newValue); |
100 | 146 | } catch (e) { |
101 | | - onParseError(event.newValue, e) |
102 | | - return |
| 147 | + onParseError(event.newValue, e); |
| 148 | + return; |
103 | 149 | } |
104 | | - const processedVal = beforeRead(newVal) |
| 150 | + const processedVal = beforeRead(newVal); |
105 | 151 |
|
106 | | - set(processedVal) |
| 152 | + set(processedVal); |
107 | 153 | } |
108 | | - } |
| 154 | + }; |
109 | 155 |
|
110 | | - window.addEventListener("storage", handleStorage) |
| 156 | + window.addEventListener("storage", handleStorage); |
111 | 157 |
|
112 | | - return () => window.removeEventListener("storage", handleStorage) |
| 158 | + return () => window.removeEventListener("storage", handleStorage); |
113 | 159 | } |
114 | | - }) |
| 160 | + }); |
115 | 161 |
|
116 | | - const { subscribe, set } = store |
| 162 | + const { subscribe, set } = store; |
117 | 163 |
|
118 | 164 | 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); |
122 | 168 | }, |
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))); |
131 | 172 | }, |
132 | | - reset() { |
133 | | - this.set(initialValue) |
| 173 | + async reset() { |
| 174 | + await this.set(initialValue); |
134 | 175 | }, |
135 | | - subscribe |
136 | | - } |
| 176 | + subscribe, |
| 177 | + }; |
137 | 178 | } |
138 | | - return stores[storageType][key] |
| 179 | + return stores[storageType][key]; |
139 | 180 | } |
0 commit comments