Skip to content

Commit 15895a1

Browse files
feat: SF cache persistence attempt
1 parent c2afeea commit 15895a1

3 files changed

Lines changed: 143 additions & 8 deletions

File tree

next.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ module.exports = withTM({
2828
},
2929
async headers() {
3030
return [
31+
{
32+
source: '/stockfish/:path*',
33+
headers: [
34+
{
35+
key: 'Cache-Control',
36+
value: 'public, max-age=31536000, immutable',
37+
},
38+
],
39+
},
3140
{
3241
source: '/(.*)',
3342
headers: [

src/lib/engine/stockfish.ts

Lines changed: 31 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,25 @@ const sharedWasmMemory = (lo: number, hi = 32767): WebAssembly.Memory => {
339340
}
340341
}
341342

343+
const loadNnueModel = async (
344+
url: string,
345+
storage: StockfishModelStorage,
346+
): Promise<ArrayBuffer> => {
347+
const cachedBuffer = await storage.getModel(url)
348+
if (cachedBuffer) {
349+
return cachedBuffer
350+
}
351+
352+
const response = await fetch(url, { cache: 'force-cache' })
353+
if (!response.ok) {
354+
throw new Error(`Failed to fetch Stockfish NNUE model: ${url}`)
355+
}
356+
357+
const buffer = await response.arrayBuffer()
358+
await storage.storeModel(url, buffer)
359+
return buffer
360+
}
361+
342362
const setupStockfish = (): Promise<StockfishWeb> => {
343363
return new Promise<StockfishWeb>((resolve, reject) => {
344364
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -351,16 +371,19 @@ const setupStockfish = (): Promise<StockfishWeb> => {
351371
})
352372
.then(async (instance: StockfishWeb) => {
353373
// Load NNUE models before resolving
374+
const storage = new StockfishModelStorage()
375+
await storage.requestPersistentStorage()
376+
354377
Promise.all([
355-
fetch(`/stockfish/${instance.getRecommendedNnue(0)}`),
356-
fetch(`/stockfish/${instance.getRecommendedNnue(1)}`),
378+
loadNnueModel(
379+
`/stockfish/${instance.getRecommendedNnue(0)}`,
380+
storage,
381+
),
382+
loadNnueModel(
383+
`/stockfish/${instance.getRecommendedNnue(1)}`,
384+
storage,
385+
),
357386
])
358-
.then((responses) => {
359-
return Promise.all([
360-
responses[0].arrayBuffer(),
361-
responses[1].arrayBuffer(),
362-
])
363-
})
364387
.then((buffers) => {
365388
instance.setNnueBuffer(new Uint8Array(buffers[0]), 0)
366389
instance.setNnueBuffer(new Uint8Array(buffers[1]), 1)

src/lib/engine/stockfishStorage.ts

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

0 commit comments

Comments
 (0)