mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-03-11 22:15:35 +00:00
feat: new more performant cache
This commit is contained in:
parent
49a5e26155
commit
77dc3c28f3
4 changed files with 199 additions and 7 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "6.4.155",
|
||||
"version": "6.4.156",
|
||||
"license": "BUSL-1.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.5",
|
||||
|
|
|
|||
191
src/lib/modules/anilist/storage.ts
Normal file
191
src/lib/modules/anilist/storage.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
SerializedEntries,
|
||||
SerializedRequest,
|
||||
StorageAdapter
|
||||
} from '@urql/exchange-graphcache'
|
||||
|
||||
const getRequestPromise = <T>(request: IDBRequest<T>): Promise<T> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = () => {
|
||||
reject(request.error as Error)
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getTransactionPromise = (transaction: IDBTransaction): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error as 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
|
||||
/** Gets Called when the exchange has hydrated the data from storage. */
|
||||
onCacheHydrated?: () => void
|
||||
}
|
||||
|
||||
/** 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 => {
|
||||
opts ??= {}
|
||||
|
||||
let callback: (() => void) | undefined
|
||||
|
||||
const DB_NAME = opts.idbName || 'graphcache-v4'
|
||||
const ENTRIES_STORE_NAME = 'entries'
|
||||
const METADATA_STORE_NAME = 'metadata'
|
||||
|
||||
let batch: SerializedEntries = 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)
|
||||
}
|
||||
|
||||
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(batch, timestamp)
|
||||
)
|
||||
})
|
||||
.then(toUndefined, toUndefined)
|
||||
},
|
||||
|
||||
readData (): Promise<SerializedEntries> {
|
||||
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)
|
||||
request.onsuccess = () => {
|
||||
if (key === timestamp) { Object.assign(batch, request.result) }
|
||||
}
|
||||
}
|
||||
|
||||
this.result.continue()
|
||||
}
|
||||
}
|
||||
|
||||
return getTransactionPromise(transaction)
|
||||
})
|
||||
.then(
|
||||
() => batch,
|
||||
() => batch
|
||||
)
|
||||
},
|
||||
onCacheHydrated: opts.onCacheHydrated,
|
||||
onOnline (cb: () => void) {
|
||||
if (callback) {
|
||||
window.removeEventListener('online', callback)
|
||||
callback = undefined
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
'online',
|
||||
(callback = () => {
|
||||
cb()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { authExchange } from '@urql/exchange-auth'
|
||||
import { offlineExchange } from '@urql/exchange-graphcache'
|
||||
import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'
|
||||
import { Client, fetchExchange } from '@urql/svelte'
|
||||
import Bottleneck from 'bottleneck'
|
||||
import Debug from 'debug'
|
||||
|
|
@ -11,6 +10,7 @@ import gql from './gql'
|
|||
import { CommentFrag, CustomLists, type Entry, FullMedia, FullMediaList, ThreadFrag, type ToggleFavourite, UserLists, Viewer } from './queries'
|
||||
import { refocusExchange } from './refocus'
|
||||
import schema from './schema.json' with { type: 'json' }
|
||||
import { makeDefaultStorage } from './storage'
|
||||
|
||||
import type { ResultOf } from 'gql.tada'
|
||||
|
||||
|
|
@ -34,11 +34,13 @@ class FetchError extends Error {
|
|||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
export const storagePromise = Promise.withResolvers<void>()
|
||||
export const storage = makeDefaultStorage({
|
||||
idbName: 'graphcache-v3',
|
||||
idbName: 'anilist-cache-v1',
|
||||
onCacheHydrated: () => storagePromise.resolve(),
|
||||
maxAge: 14 // The maximum age of the persisted data in days
|
||||
})
|
||||
|
||||
indexedDB.deleteDatabase('graphcache-v3') // old version
|
||||
|
||||
debug('Loading urql client')
|
||||
storagePromise.promise.finally(() => {
|
||||
debug('Graphcache storage initialized')
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
<div class='size-full flex justify-center items-center'>
|
||||
<div class='size-10 relative logo-container' on:animationend|self={navigate}>
|
||||
<Logo class='size-10' />
|
||||
<Logo class='size-10 [filter:url(#chromaticAberration)]' />
|
||||
{#each spotlightData as s, i (i)}
|
||||
<div class='spotlight absolute blurred origin-left'
|
||||
style:--to-x={s.tox}
|
||||
|
|
@ -51,7 +51,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<svg width='0' height='0'>
|
||||
<filter id='chromaticAberration'>
|
||||
<feColorMatrix type='matrix'
|
||||
|
|
@ -83,7 +82,7 @@
|
|||
</feOffset>
|
||||
<feBlend mode='screen' in='red' in2='blue' />
|
||||
</filter>
|
||||
</svg> -->
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
@property --spotlight-opacity {
|
||||
|
|
@ -132,7 +131,7 @@
|
|||
}
|
||||
|
||||
.spotlight {
|
||||
filter: blur(3px);
|
||||
filter: url('#chromaticAberration') blur(3px);
|
||||
--impect-radius: 192;
|
||||
--dist-factor: calc((192 - min(var(--to-dist), 192)) / 192);
|
||||
left: calc(var(--to-x) * 1px);
|
||||
|
|
|
|||
Loading…
Reference in a new issue