Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 5.6 kB view raw
1import type { 2 SerializedEntries, 3 SerializedRequest, 4 StorageAdapter, 5} from '../types'; 6 7const getRequestPromise = <T>(request: IDBRequest<T>): Promise<T> => { 8 return new Promise((resolve, reject) => { 9 request.onerror = () => { 10 reject(request.error); 11 }; 12 13 request.onsuccess = () => { 14 resolve(request.result); 15 }; 16 }); 17}; 18 19const getTransactionPromise = (transaction: IDBTransaction): Promise<any> => { 20 return new Promise((resolve, reject) => { 21 transaction.onerror = () => { 22 reject(transaction.error); 23 }; 24 25 transaction.oncomplete = resolve; 26 }); 27}; 28 29export interface StorageOptions { 30 /** Name of the IndexedDB database that will be used. 31 * @defaultValue `'graphcache-v4'` 32 */ 33 idbName?: string; 34 /** Maximum age of cache entries (in days) after which data is discarded. 35 * @defaultValue `7` days 36 */ 37 maxAge?: number; 38 /** Gets Called when the exchange has hydrated the data from storage. */ 39 onCacheHydrated?: () => void; 40} 41 42/** Sample storage adapter persisting to IndexedDB. */ 43export interface DefaultStorage extends StorageAdapter { 44 /** Clears the entire IndexedDB storage. */ 45 clear(): Promise<any>; 46} 47 48/** Creates a default {@link StorageAdapter} which uses IndexedDB for storage. 49 * 50 * @param opts - A {@link StorageOptions} configuration object. 51 * @returns the created {@link StorageAdapter}. 52 * 53 * @remarks 54 * The default storage uses IndexedDB to persist the normalized cache for 55 * offline use. It demonstrates that the cache can be chunked by timestamps. 56 * 57 * Note: We have no data on stability of this storage and our Offline Support 58 * for large APIs or longterm use. Proceed with caution. 59 */ 60export const makeDefaultStorage = (opts?: StorageOptions): DefaultStorage => { 61 if (!opts) opts = {}; 62 63 let callback: (() => void) | undefined; 64 65 const DB_NAME = opts.idbName || 'graphcache-v4'; 66 const ENTRIES_STORE_NAME = 'entries'; 67 const METADATA_STORE_NAME = 'metadata'; 68 69 let batch: Record<string, string | undefined> = Object.create(null); 70 const timestamp = Math.floor(new Date().valueOf() / (1000 * 60 * 60 * 24)); 71 const maxAge = timestamp - (opts.maxAge || 7); 72 73 const req = indexedDB.open(DB_NAME, 1); 74 const database$ = getRequestPromise(req); 75 76 req.onupgradeneeded = () => { 77 req.result.createObjectStore(ENTRIES_STORE_NAME); 78 req.result.createObjectStore(METADATA_STORE_NAME); 79 }; 80 81 return { 82 clear() { 83 return database$.then(database => { 84 const transaction = database.transaction( 85 [METADATA_STORE_NAME, ENTRIES_STORE_NAME], 86 'readwrite' 87 ); 88 transaction.objectStore(METADATA_STORE_NAME).clear(); 89 transaction.objectStore(ENTRIES_STORE_NAME).clear(); 90 batch = Object.create(null); 91 return getTransactionPromise(transaction); 92 }); 93 }, 94 95 readMetadata(): Promise<null | SerializedRequest[]> { 96 return database$.then( 97 database => { 98 return getRequestPromise<SerializedRequest[]>( 99 database 100 .transaction(METADATA_STORE_NAME, 'readonly') 101 .objectStore(METADATA_STORE_NAME) 102 .get(METADATA_STORE_NAME) 103 ); 104 }, 105 () => null 106 ); 107 }, 108 109 writeMetadata(metadata: SerializedRequest[]) { 110 database$.then( 111 database => { 112 return getRequestPromise( 113 database 114 .transaction(METADATA_STORE_NAME, 'readwrite') 115 .objectStore(METADATA_STORE_NAME) 116 .put(metadata, METADATA_STORE_NAME) 117 ); 118 }, 119 () => { 120 /* noop */ 121 } 122 ); 123 }, 124 125 writeData(entries: SerializedEntries): Promise<void> { 126 Object.assign(batch, entries); 127 const toUndefined = () => undefined; 128 129 return database$ 130 .then(database => { 131 return getRequestPromise( 132 database 133 .transaction(ENTRIES_STORE_NAME, 'readwrite') 134 .objectStore(ENTRIES_STORE_NAME) 135 .put(batch, timestamp) 136 ); 137 }) 138 .then(toUndefined, toUndefined); 139 }, 140 141 readData(): Promise<SerializedEntries> { 142 const data: SerializedEntries = {}; 143 return database$ 144 .then(database => { 145 const transaction = database.transaction( 146 ENTRIES_STORE_NAME, 147 'readwrite' 148 ); 149 150 const store = transaction.objectStore(ENTRIES_STORE_NAME); 151 const request = (store.openKeyCursor || store.openCursor).call(store); 152 153 request.onsuccess = function () { 154 if (this.result) { 155 const { key } = this.result; 156 if (typeof key !== 'number' || key < maxAge) { 157 store.delete(key); 158 } else { 159 const request = store.get(key); 160 request.onsuccess = () => { 161 const result = request.result; 162 if (key === timestamp) Object.assign(batch, result); 163 Object.assign(data, result); 164 }; 165 } 166 167 this.result.continue(); 168 } 169 }; 170 171 return getTransactionPromise(transaction); 172 }) 173 .then( 174 () => data, 175 () => batch 176 ); 177 }, 178 onCacheHydrated: opts.onCacheHydrated, 179 onOnline(cb: () => void) { 180 if (callback) { 181 window.removeEventListener('online', callback); 182 callback = undefined; 183 } 184 185 window.addEventListener( 186 'online', 187 (callback = () => { 188 cb(); 189 }) 190 ); 191 }, 192 }; 193};