fuzzyworld

tower / files / src / terrain / cache.ts

cache.ts

2.5 KB · history · blame

1/**
2 * The compiled world, kept in IndexedDB between visits. See world-generation.md §5.4.
3 *
4 * Compiling is the slow part of loading — mostly the Girdle's erosion, a few seconds —
5 * and its result depends only on the world file and the code that compiles it. So it is
6 * stored under a key made of both, and a returning visit with the same world and the same
7 * build skips it. Nothing here is authoritative: a miss, a quota error or storage that is
8 * not there at all just means compiling again.
9 */
10import type { CompiledWorld } from './compile.js'
11
12const DB = 'tower-terrain'
13const STORE = 'compiled'
14/** Entries kept. The author's edits make new worlds constantly; old ones are not wanted. */
15const KEEP = 3
16
17function open(): Promise<IDBDatabase> {
18 return new Promise((resolve, reject) => {
19 const req = indexedDB.open(DB, 1)
20 req.onupgradeneeded = () => req.result.createObjectStore(STORE)
21 req.onsuccess = () => resolve(req.result)
22 req.onerror = () => reject(req.error)
23 })
24}
25
26/** A short stable key for a long string: FNV-1a, twice, 64 bits of it in hex. */
27export function cacheKey(text: string): string {
28 let a = 0x811c9dc5, b = 0x9dc5811c
29 for (let i = 0; i < text.length; i++) {
30 const c = text.charCodeAt(i)
31 a = Math.imul(a ^ c, 16777619)
32 b = Math.imul(b ^ c, 16777619) ^ (a >>> 13)
33 }
34 return `${(a >>> 0).toString(16).padStart(8, '0')}${(b >>> 0).toString(16).padStart(8, '0')}`
35}
36
37export async function loadCompiled(key: string): Promise<CompiledWorld | null> {
38 try {
39 const db = await open()
40 return await new Promise((resolve) => {
41 const get = db.transaction(STORE).objectStore(STORE).get(key)
42 get.onsuccess = () => resolve((get.result as { world: CompiledWorld } | undefined)?.world ?? null)
43 get.onerror = () => resolve(null)
44 })
45 } catch {
46 return null
47 }
48}
49
50export async function saveCompiled(key: string, world: CompiledWorld): Promise<void> {
51 try {
52 const db = await open()
53 const store = db.transaction(STORE, 'readwrite').objectStore(STORE)
54 store.put({ world, at: Date.now() }, key)
55 // Drop all but the newest few.
56 const all = store.openCursor()
57 const entries: { key: IDBValidKey; at: number }[] = []
58 all.onsuccess = () => {
59 const cursor = all.result
60 if (cursor) {
61 entries.push({ key: cursor.key, at: (cursor.value as { at: number }).at })
62 cursor.continue()
63 return
64 }
65 entries.sort((x, y) => y.at - x.at)
66 for (const e of entries.slice(KEEP)) store.delete(e.key)
67 }
68 } catch {
69 // Not caching is only slower.
70 }
71}