forked from urql-graphql/urql
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(graphcache): SerializedEntries as Map
Swap SerializedEntries to pass data as a Map instead plain object. The main motivation for this change is to support metro's low object properties limit. facebook/hermes#851 Urql issue: urql-graphql#3425
- Loading branch information
Showing
7 changed files
with
24 additions
and
259 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@urql/exchange-graphcache': major | ||
--- | ||
|
||
Use Map over plain JS object in StorageAdapter. Any custom StorageAdapter will have to update to | ||
support `readData` and `writeData` as a Map. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,237 +1 @@ | ||
import type { | ||
SerializedEntries, | ||
SerializedRequest, | ||
StorageAdapter, | ||
} from '../types'; | ||
|
||
const getRequestPromise = <T>(request: IDBRequest<T>): Promise<T> => { | ||
return new Promise((resolve, reject) => { | ||
request.onerror = () => { | ||
reject(request.error); | ||
}; | ||
|
||
request.onsuccess = () => { | ||
resolve(request.result); | ||
}; | ||
}); | ||
}; | ||
|
||
const getTransactionPromise = (transaction: IDBTransaction): Promise<any> => { | ||
return new Promise((resolve, reject) => { | ||
transaction.onerror = () => { | ||
reject(transaction.error); | ||
}; | ||
|
||
transaction.oncomplete = resolve; | ||
}); | ||
}; | ||
|
||
export interface StorageOptions { | ||
/** Name of the IndexedDB database that will be used. | ||
* @defaultValue `'graphcache-v4'` | ||
*/ | ||
idbName?: string; | ||
/** Maximum age of cache entries (in days) after which data is discarded. | ||
* @defaultValue `7` days | ||
*/ | ||
maxAge?: number; | ||
} | ||
|
||
/** Sample storage adapter persisting to IndexedDB. */ | ||
export interface DefaultStorage extends StorageAdapter { | ||
/** Clears the entire IndexedDB storage. */ | ||
clear(): Promise<any>; | ||
} | ||
|
||
/** Creates a default {@link StorageAdapter} which uses IndexedDB for storage. | ||
* | ||
* @param opts - A {@link StorageOptions} configuration object. | ||
* @returns the created {@link StorageAdapter}. | ||
* | ||
* @remarks | ||
* The default storage uses IndexedDB to persist the normalized cache for | ||
* offline use. It demonstrates that the cache can be chunked by timestamps. | ||
* | ||
* Note: We have no data on stability of this storage and our Offline Support | ||
* for large APIs or longterm use. Proceed with caution. | ||
*/ | ||
export const makeDefaultStorage = (opts?: StorageOptions): DefaultStorage => { | ||
if (!opts) opts = {}; | ||
|
||
let callback: (() => void) | undefined; | ||
|
||
const DB_NAME = opts.idbName || 'graphcache-v4'; | ||
const ENTRIES_STORE_NAME = 'entries'; | ||
const METADATA_STORE_NAME = 'metadata'; | ||
|
||
let batch: Record<string, string | undefined> = Object.create(null); | ||
const timestamp = Math.floor(new Date().valueOf() / (1000 * 60 * 60 * 24)); | ||
const maxAge = timestamp - (opts.maxAge || 7); | ||
|
||
const req = indexedDB.open(DB_NAME, 1); | ||
const database$ = getRequestPromise(req); | ||
|
||
req.onupgradeneeded = () => { | ||
req.result.createObjectStore(ENTRIES_STORE_NAME); | ||
req.result.createObjectStore(METADATA_STORE_NAME); | ||
}; | ||
|
||
const serializeEntry = (entry: string): string => entry.replace(/:/g, '%3a'); | ||
|
||
const deserializeEntry = (entry: string): string => | ||
entry.replace(/%3a/g, ':'); | ||
|
||
const serializeBatch = (): string => { | ||
let data = ''; | ||
for (const key in batch) { | ||
const value = batch[key]; | ||
data += serializeEntry(key); | ||
data += ':'; | ||
if (value) data += serializeEntry(value); | ||
data += ':'; | ||
} | ||
|
||
return data; | ||
}; | ||
|
||
const deserializeBatch = (input: string) => { | ||
const data = {}; | ||
let char = ''; | ||
let key = ''; | ||
let entry = ''; | ||
let mode = 0; | ||
let index = 0; | ||
while (index < input.length) { | ||
entry = ''; | ||
while ((char = input[index++]) !== ':' && char) { | ||
entry += char; | ||
} | ||
|
||
if (mode) { | ||
data[key] = deserializeEntry(entry) || undefined; | ||
mode = 0; | ||
} else { | ||
key = deserializeEntry(entry); | ||
mode = 1; | ||
} | ||
} | ||
|
||
return data; | ||
}; | ||
|
||
return { | ||
clear() { | ||
return database$.then(database => { | ||
const transaction = database.transaction( | ||
[METADATA_STORE_NAME, ENTRIES_STORE_NAME], | ||
'readwrite' | ||
); | ||
transaction.objectStore(METADATA_STORE_NAME).clear(); | ||
transaction.objectStore(ENTRIES_STORE_NAME).clear(); | ||
batch = Object.create(null); | ||
return getTransactionPromise(transaction); | ||
}); | ||
}, | ||
|
||
readMetadata(): Promise<null | SerializedRequest[]> { | ||
return database$.then( | ||
database => { | ||
return getRequestPromise<SerializedRequest[]>( | ||
database | ||
.transaction(METADATA_STORE_NAME, 'readonly') | ||
.objectStore(METADATA_STORE_NAME) | ||
.get(METADATA_STORE_NAME) | ||
); | ||
}, | ||
() => null | ||
); | ||
}, | ||
|
||
writeMetadata(metadata: SerializedRequest[]) { | ||
database$.then( | ||
database => { | ||
return getRequestPromise( | ||
database | ||
.transaction(METADATA_STORE_NAME, 'readwrite') | ||
.objectStore(METADATA_STORE_NAME) | ||
.put(metadata, METADATA_STORE_NAME) | ||
); | ||
}, | ||
() => { | ||
/* noop */ | ||
} | ||
); | ||
}, | ||
|
||
writeData(entries: SerializedEntries): Promise<void> { | ||
Object.assign(batch, entries); | ||
const toUndefined = () => undefined; | ||
|
||
return database$ | ||
.then(database => { | ||
return getRequestPromise( | ||
database | ||
.transaction(ENTRIES_STORE_NAME, 'readwrite') | ||
.objectStore(ENTRIES_STORE_NAME) | ||
.put(serializeBatch(), timestamp) | ||
); | ||
}) | ||
.then(toUndefined, toUndefined); | ||
}, | ||
|
||
readData(): Promise<SerializedEntries> { | ||
const chunks: string[] = []; | ||
return database$ | ||
.then(database => { | ||
const transaction = database.transaction( | ||
ENTRIES_STORE_NAME, | ||
'readwrite' | ||
); | ||
|
||
const store = transaction.objectStore(ENTRIES_STORE_NAME); | ||
const request = (store.openKeyCursor || store.openCursor).call(store); | ||
|
||
request.onsuccess = function () { | ||
if (this.result) { | ||
const { key } = this.result; | ||
if (typeof key !== 'number' || key < maxAge) { | ||
store.delete(key); | ||
} else { | ||
const request = store.get(key); | ||
const index = chunks.length; | ||
chunks.push(''); | ||
request.onsuccess = () => { | ||
const result = '' + request.result; | ||
if (key === timestamp) | ||
Object.assign(batch, deserializeBatch(result)); | ||
chunks[index] = result; | ||
}; | ||
} | ||
|
||
this.result.continue(); | ||
} | ||
}; | ||
|
||
return getTransactionPromise(transaction); | ||
}) | ||
.then( | ||
() => deserializeBatch(chunks.join('')), | ||
() => batch | ||
); | ||
}, | ||
|
||
onOnline(cb: () => void) { | ||
if (callback) { | ||
window.removeEventListener('online', callback); | ||
callback = undefined; | ||
} | ||
|
||
window.addEventListener( | ||
'online', | ||
(callback = () => { | ||
cb(); | ||
}) | ||
); | ||
}, | ||
}; | ||
}; | ||
// Removed. Not supported in Serialized Entries as Map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 9 additions & 9 deletions
18
exchanges/graphcache/src/store/__snapshots__/store.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,18 @@ | ||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
||
exports[`Store with storage > should be able to persist embedded data 1`] = ` | ||
{ | ||
"Query%2eappointment({\\"id\\":\\"1\\"}).__typename": "\\"Appointment\\"", | ||
"Query%2eappointment({\\"id\\":\\"1\\"}).info": "\\"urql meeting\\"", | ||
"Query.appointment({\\"id\\":\\"1\\"})": ":\\"Query.appointment({\\\\\\"id\\\\\\":\\\\\\"1\\\\\\"})\\"", | ||
Map { | ||
"Query%2eappointment({\\"id\\":\\"1\\"}).__typename" => "\\"Appointment\\"", | ||
"Query%2eappointment({\\"id\\":\\"1\\"}).info" => "\\"urql meeting\\"", | ||
"Query.appointment({\\"id\\":\\"1\\"})" => ":\\"Query.appointment({\\\\\\"id\\\\\\":\\\\\\"1\\\\\\"})\\"", | ||
} | ||
`; | ||
|
||
exports[`Store with storage > should be able to store and rehydrate data 1`] = ` | ||
{ | ||
"Appointment:1.__typename": "\\"Appointment\\"", | ||
"Appointment:1.id": "\\"1\\"", | ||
"Appointment:1.info": "\\"urql meeting\\"", | ||
"Query.appointment({\\"id\\":\\"1\\"})": ":\\"Appointment:1\\"", | ||
Map { | ||
"Appointment:1.__typename" => "\\"Appointment\\"", | ||
"Appointment:1.id" => "\\"1\\"", | ||
"Appointment:1.info" => "\\"urql meeting\\"", | ||
"Query.appointment({\\"id\\":\\"1\\"})" => ":\\"Appointment:1\\"", | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters