Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 3e4bee3e9d344f738eb9c179921f75ccd8fb79c5 239 lines 6.7 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 const serializeEntry = (entry: string): string => entry.replace(/:/g, '%3a'); 82 83 const deserializeEntry = (entry: string): string => 84 entry.replace(/%3a/g, ':'); 85 86 const serializeBatch = (): string => { 87 let data = ''; 88 for (const key in batch) { 89 const value = batch[key]; 90 data += serializeEntry(key); 91 data += ':'; 92 if (value) data += serializeEntry(value); 93 data += ':'; 94 } 95 96 return data; 97 }; 98 99 const deserializeBatch = (input: string) => { 100 const data = {}; 101 let char = ''; 102 let key = ''; 103 let entry = ''; 104 let mode = 0; 105 let index = 0; 106 while (index < input.length) { 107 entry = ''; 108 while ((char = input[index++]) !== ':' && char) { 109 entry += char; 110 } 111 112 if (mode) { 113 data[key] = deserializeEntry(entry) || undefined; 114 mode = 0; 115 } else { 116 key = deserializeEntry(entry); 117 mode = 1; 118 } 119 } 120 121 return data; 122 }; 123 124 return { 125 clear() { 126 return database$.then(database => { 127 const transaction = database.transaction( 128 [METADATA_STORE_NAME, ENTRIES_STORE_NAME], 129 'readwrite' 130 ); 131 transaction.objectStore(METADATA_STORE_NAME).clear(); 132 transaction.objectStore(ENTRIES_STORE_NAME).clear(); 133 batch = Object.create(null); 134 return getTransactionPromise(transaction); 135 }); 136 }, 137 138 readMetadata(): Promise<null | SerializedRequest[]> { 139 return database$.then( 140 database => { 141 return getRequestPromise<SerializedRequest[]>( 142 database 143 .transaction(METADATA_STORE_NAME, 'readonly') 144 .objectStore(METADATA_STORE_NAME) 145 .get(METADATA_STORE_NAME) 146 ); 147 }, 148 () => null 149 ); 150 }, 151 152 writeMetadata(metadata: SerializedRequest[]) { 153 database$.then( 154 database => { 155 return getRequestPromise( 156 database 157 .transaction(METADATA_STORE_NAME, 'readwrite') 158 .objectStore(METADATA_STORE_NAME) 159 .put(metadata, METADATA_STORE_NAME) 160 ); 161 }, 162 () => { 163 /* noop */ 164 } 165 ); 166 }, 167 168 writeData(entries: SerializedEntries): Promise<void> { 169 Object.assign(batch, entries); 170 const toUndefined = () => undefined; 171 172 return database$ 173 .then(database => { 174 return getRequestPromise( 175 database 176 .transaction(ENTRIES_STORE_NAME, 'readwrite') 177 .objectStore(ENTRIES_STORE_NAME) 178 .put(serializeBatch(), timestamp) 179 ); 180 }) 181 .then(toUndefined, toUndefined); 182 }, 183 184 readData(): Promise<SerializedEntries> { 185 const chunks: string[] = []; 186 return database$ 187 .then(database => { 188 const transaction = database.transaction( 189 ENTRIES_STORE_NAME, 190 'readwrite' 191 ); 192 193 const store = transaction.objectStore(ENTRIES_STORE_NAME); 194 const request = (store.openKeyCursor || store.openCursor).call(store); 195 196 request.onsuccess = function () { 197 if (this.result) { 198 const { key } = this.result; 199 if (typeof key !== 'number' || key < maxAge) { 200 store.delete(key); 201 } else { 202 const request = store.get(key); 203 const index = chunks.length; 204 chunks.push(''); 205 request.onsuccess = () => { 206 const result = '' + request.result; 207 if (key === timestamp) 208 Object.assign(batch, deserializeBatch(result)); 209 chunks[index] = result; 210 }; 211 } 212 213 this.result.continue(); 214 } 215 }; 216 217 return getTransactionPromise(transaction); 218 }) 219 .then( 220 () => deserializeBatch(chunks.join('')), 221 () => batch 222 ); 223 }, 224 onCacheHydrated: opts.onCacheHydrated, 225 onOnline(cb: () => void) { 226 if (callback) { 227 window.removeEventListener('online', callback); 228 callback = undefined; 229 } 230 231 window.addEventListener( 232 'online', 233 (callback = () => { 234 cb(); 235 }) 236 ); 237 }, 238 }; 239};