Skip to content

Commit f77f11d

Browse files
Merge pull request #227 from CSSLab/codex/stockfish-cache-onetime-download
feat: Cache Stockfish NNUE models in IndexedDB
2 parents 038bd72 + 5bf585e commit f77f11d

2 files changed

Lines changed: 164 additions & 8 deletions

File tree

src/lib/engine/stockfish.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Chess } from 'chess.ts'
22
import { cpToWinrate } from 'src/lib'
33
import StockfishWeb from 'lila-stockfish-web'
44
import { StockfishEvaluation } from 'src/types'
5+
import { StockfishModelStorage } from './stockfishStorage'
56

67
class Engine {
78
private fen: string
@@ -339,6 +340,27 @@ const sharedWasmMemory = (lo: number, hi = 32767): WebAssembly.Memory => {
339340
}
340341
}
341342

343+
const loadNnueModel = async (
344+
modelUrl: string,
345+
storage: StockfishModelStorage,
346+
): Promise<ArrayBuffer> => {
347+
const cachedModel = await storage.getModel(modelUrl)
348+
if (cachedModel) {
349+
return cachedModel
350+
}
351+
352+
const response = await fetch(modelUrl)
353+
if (!response.ok) {
354+
throw new Error(
355+
`Failed to fetch Stockfish NNUE model (${response.status}) from ${modelUrl}`,
356+
)
357+
}
358+
359+
const buffer = await response.arrayBuffer()
360+
await storage.storeModel(modelUrl, buffer)
361+
return buffer
362+
}
363+
342364
const setupStockfish = (): Promise<StockfishWeb> => {
343365
return new Promise<StockfishWeb>((resolve, reject) => {
344366
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -355,17 +377,17 @@ const setupStockfish = (): Promise<StockfishWeb> => {
355377
const nnueBaseUrl =
356378
process.env.NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL ??
357379
'https://raw.githubusercontent.com/CSSLab/maia-platform-frontend/e23a50e/public/stockfish'
380+
const storage = new StockfishModelStorage()
381+
await storage.requestPersistentStorage()
382+
383+
const nnue0Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(0)}`
384+
const nnue1Url = `${nnueBaseUrl}/${instance.getRecommendedNnue(1)}`
385+
358386
// Load NNUE models before resolving
359387
Promise.all([
360-
fetch(`${nnueBaseUrl}/${instance.getRecommendedNnue(0)}`),
361-
fetch(`${nnueBaseUrl}/${instance.getRecommendedNnue(1)}`),
388+
loadNnueModel(nnue0Url, storage),
389+
loadNnueModel(nnue1Url, storage),
362390
])
363-
.then((responses) => {
364-
return Promise.all([
365-
responses[0].arrayBuffer(),
366-
responses[1].arrayBuffer(),
367-
])
368-
})
369391
.then((buffers) => {
370392
instance.setNnueBuffer(new Uint8Array(buffers[0]), 0)
371393
instance.setNnueBuffer(new Uint8Array(buffers[1]), 1)

src/lib/engine/stockfishStorage.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
interface NnueStorage {
2+
id: string
3+
url: string
4+
data: Blob
5+
timestamp: number
6+
size: number
7+
}
8+
9+
export class StockfishModelStorage {
10+
private dbName = 'StockfishModels'
11+
private storeName = 'models'
12+
private version = 1
13+
private db: IDBDatabase | null = null
14+
15+
async openDB(): Promise<IDBDatabase | null> {
16+
if (typeof indexedDB === 'undefined') {
17+
return null
18+
}
19+
20+
if (this.db) return this.db
21+
22+
return new Promise((resolve, reject) => {
23+
const request = indexedDB.open(this.dbName, this.version)
24+
25+
request.onerror = () => reject(request.error)
26+
request.onsuccess = () => {
27+
this.db = request.result
28+
resolve(request.result)
29+
}
30+
31+
request.onupgradeneeded = (event) => {
32+
const db = (event.target as IDBOpenDBRequest).result
33+
if (!db.objectStoreNames.contains(this.storeName)) {
34+
const store = db.createObjectStore(this.storeName, { keyPath: 'id' })
35+
store.createIndex('timestamp', 'timestamp', { unique: false })
36+
}
37+
}
38+
})
39+
}
40+
41+
async getModel(modelUrl: string): Promise<ArrayBuffer | null> {
42+
try {
43+
const db = await this.openDB()
44+
if (!db) return null
45+
46+
const transaction = db.transaction([this.storeName], 'readonly')
47+
const store = transaction.objectStore(this.storeName)
48+
49+
const modelData = await new Promise<NnueStorage | null>(
50+
(resolve, reject) => {
51+
const request = store.get(modelUrl)
52+
request.onsuccess = () => resolve(request.result || null)
53+
request.onerror = () => reject(request.error)
54+
},
55+
)
56+
57+
if (!modelData) {
58+
return null
59+
}
60+
61+
if (modelData.url !== modelUrl) {
62+
await this.deleteModel(modelUrl)
63+
return null
64+
}
65+
66+
return modelData.data.arrayBuffer()
67+
} catch (error) {
68+
console.warn('Stockfish cache read failed:', error)
69+
return null
70+
}
71+
}
72+
73+
async storeModel(modelUrl: string, buffer: ArrayBuffer): Promise<void> {
74+
try {
75+
const db = await this.openDB()
76+
if (!db) return
77+
78+
const transaction = db.transaction([this.storeName], 'readwrite')
79+
const store = transaction.objectStore(this.storeName)
80+
81+
const modelData: NnueStorage = {
82+
id: modelUrl,
83+
url: modelUrl,
84+
data: new Blob([buffer]),
85+
timestamp: Date.now(),
86+
size: buffer.byteLength,
87+
}
88+
89+
await new Promise<void>((resolve, reject) => {
90+
const request = store.put(modelData)
91+
request.onsuccess = () => resolve()
92+
request.onerror = () => reject(request.error)
93+
})
94+
} catch (error) {
95+
console.warn('Stockfish cache write failed:', error)
96+
}
97+
}
98+
99+
async deleteModel(modelUrl: string): Promise<void> {
100+
try {
101+
const db = await this.openDB()
102+
if (!db) return
103+
104+
const transaction = db.transaction([this.storeName], 'readwrite')
105+
const store = transaction.objectStore(this.storeName)
106+
107+
await new Promise<void>((resolve, reject) => {
108+
const request = store.delete(modelUrl)
109+
request.onsuccess = () => resolve()
110+
request.onerror = () => reject(request.error)
111+
})
112+
} catch (error) {
113+
console.warn('Stockfish cache delete failed:', error)
114+
}
115+
}
116+
117+
async requestPersistentStorage(): Promise<boolean> {
118+
try {
119+
if (
120+
typeof navigator !== 'undefined' &&
121+
'storage' in navigator &&
122+
'persist' in navigator.storage
123+
) {
124+
return navigator.storage.persist()
125+
}
126+
return false
127+
} catch (error) {
128+
console.warn('Failed to request persistent storage:', error)
129+
return false
130+
}
131+
}
132+
}
133+
134+
export default StockfishModelStorage

0 commit comments

Comments
 (0)