You've already forked AstralRinth
forked from didirus/AstralRinth
* feat: only initialize batch renderer if needed & head storage * feat: support webp storage of skin renders if supported (falls back to png if not) * fix: performance improvements with cache loading+saving * fix: mirrored skins + remove cape model for embedded cape * feat: antialiasing * fix: leg jumping & store fbx's for reference * fix: lint issues * fix: lint issues * feat: tweaks to radial spotlight * fix: app nav btn colors
230 lines
6.0 KiB
TypeScript
230 lines
6.0 KiB
TypeScript
interface StoredHead {
|
|
blob: Blob
|
|
timestamp: number
|
|
}
|
|
|
|
export class HeadStorage {
|
|
private dbName = 'head-storage'
|
|
private version = 1
|
|
private db: IDBDatabase | null = null
|
|
|
|
async init(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(this.dbName, this.version)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => {
|
|
this.db = request.result
|
|
resolve()
|
|
}
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result
|
|
if (!db.objectStoreNames.contains('heads')) {
|
|
db.createObjectStore('heads')
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async store(key: string, blob: Blob): Promise<void> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
|
const store = transaction.objectStore('heads')
|
|
|
|
const storedHead: StoredHead = {
|
|
blob,
|
|
timestamp: Date.now(),
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = store.put(storedHead, key)
|
|
|
|
request.onsuccess = () => resolve()
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
async retrieve(key: string): Promise<string | null> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
|
const store = transaction.objectStore('heads')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = store.get(key)
|
|
|
|
request.onsuccess = () => {
|
|
const result = request.result as StoredHead | undefined
|
|
|
|
if (!result) {
|
|
resolve(null)
|
|
return
|
|
}
|
|
|
|
const url = URL.createObjectURL(result.blob)
|
|
resolve(url)
|
|
}
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
|
const store = transaction.objectStore('heads')
|
|
const results: Record<string, Blob | null> = {}
|
|
|
|
return new Promise((resolve, _reject) => {
|
|
let completedRequests = 0
|
|
|
|
if (keys.length === 0) {
|
|
resolve(results)
|
|
return
|
|
}
|
|
|
|
for (const key of keys) {
|
|
const request = store.get(key)
|
|
|
|
request.onsuccess = () => {
|
|
const result = request.result as StoredHead | undefined
|
|
|
|
if (result) {
|
|
results[key] = result.blob
|
|
} else {
|
|
results[key] = null
|
|
}
|
|
|
|
completedRequests++
|
|
if (completedRequests === keys.length) {
|
|
resolve(results)
|
|
}
|
|
}
|
|
|
|
request.onerror = () => {
|
|
results[key] = null
|
|
completedRequests++
|
|
if (completedRequests === keys.length) {
|
|
resolve(results)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
|
const store = transaction.objectStore('heads')
|
|
let deletedCount = 0
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = store.openCursor()
|
|
|
|
request.onsuccess = (event) => {
|
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
|
|
|
if (cursor) {
|
|
const key = cursor.primaryKey as string
|
|
|
|
if (!validKeys.has(key)) {
|
|
const deleteRequest = cursor.delete()
|
|
deleteRequest.onsuccess = () => {
|
|
deletedCount++
|
|
}
|
|
deleteRequest.onerror = () => {
|
|
console.warn('Failed to delete invalid head entry:', key)
|
|
}
|
|
}
|
|
|
|
cursor.continue()
|
|
} else {
|
|
resolve(deletedCount)
|
|
}
|
|
}
|
|
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
async debugCalculateStorage(): Promise<void> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
|
const store = transaction.objectStore('heads')
|
|
|
|
let totalSize = 0
|
|
let count = 0
|
|
const entries: Array<{ key: string; size: number }> = []
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = store.openCursor()
|
|
|
|
request.onsuccess = (event) => {
|
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
|
|
|
if (cursor) {
|
|
const key = cursor.primaryKey as string
|
|
const value = cursor.value as StoredHead
|
|
|
|
const entrySize = value.blob.size
|
|
totalSize += entrySize
|
|
count++
|
|
|
|
entries.push({
|
|
key,
|
|
size: entrySize,
|
|
})
|
|
|
|
cursor.continue()
|
|
} else {
|
|
console.group('🗄️ Head Storage Debug Info')
|
|
console.log(`Total entries: ${count}`)
|
|
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
|
console.log(
|
|
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
|
)
|
|
|
|
if (entries.length > 0) {
|
|
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
|
console.log(
|
|
'Largest entry:',
|
|
sortedEntries[0].key,
|
|
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
|
)
|
|
console.log(
|
|
'Smallest entry:',
|
|
sortedEntries[sortedEntries.length - 1].key,
|
|
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
|
)
|
|
}
|
|
|
|
console.groupEnd()
|
|
resolve()
|
|
}
|
|
}
|
|
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
async clearAll(): Promise<void> {
|
|
if (!this.db) await this.init()
|
|
|
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
|
const store = transaction.objectStore('heads')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = store.clear()
|
|
|
|
request.onsuccess = () => resolve()
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
}
|
|
|
|
export const headStorage = new HeadStorage()
|