Skip to content

Commit c08d7be

Browse files
committed
Remove backward compatibility with useIndexedDBCache flag
1 parent 00a82d3 commit c08d7be

File tree

8 files changed

+84
-36
lines changed

8 files changed

+84
-36
lines changed

examples/cache-usage/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# WebLLM Cache Usage
22

33
WebLLM supports multiple persistent cache backends. You can pick the classic Cache API, IndexedDB, or the experimental Chrome [Cross-Origin Storage](https://github.com/explainers-by-googlers/cross-origin-storage) extension by
4-
setting `AppConfig.cacheBackend` to `"cache"`, `"indexeddb"`, or `"cross-origin"`. (`AppConfig.useIndexedDBCache`
5-
is still honored for backward compatibility.)
4+
setting `AppConfig.cacheBackend` to `"cache"`, `"indexeddb"`, or `"cross-origin"`.
65
This folder provides an example on how different caches are used in WebLLM. We also
76
demonstrate the utility cache functions such as deleting models, checking if models are in cache, etc.
87

examples/cache-usage/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"start": "parcel src/cache_usage.html --port 8888",
6+
"start": "parcel src/cache_usage.html --port 8889",
77
"build": "parcel build src/cache_usage.html --dist-dir lib"
88
},
99
"devDependencies": {
1010
"buffer": "^5.7.1",
11-
"parcel": "^2.8.3",
11+
"parcel": "2.8.3",
1212
"process": "^0.11.10",
1313
"tslib": "^2.3.1",
1414
"typescript": "^4.9.5",

src/cache_util.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ import CrossOriginStorageCache from "./cross_origin_storage_cache";
1515
type CacheScope = "webllm/model" | "webllm/config" | "webllm/wasm";
1616

1717
let crossOriginUnavailableLogged = false;
18+
let crossOriginAvailabilityWait: Promise<void> | null = null;
19+
20+
function scheduleCrossOriginFallbackWarning(
21+
logger: (msg: string) => void,
22+
): void {
23+
if (crossOriginUnavailableLogged || crossOriginAvailabilityWait) {
24+
return;
25+
}
26+
crossOriginAvailabilityWait = (async () => {
27+
const availableSoon = await CrossOriginStorage.waitForAvailability();
28+
crossOriginAvailabilityWait = null;
29+
if (availableSoon || crossOriginUnavailableLogged) {
30+
return;
31+
}
32+
logger(
33+
"Cross-origin storage backend is not yet available; temporarily falling back to the Cache API.",
34+
);
35+
crossOriginUnavailableLogged = true;
36+
})();
37+
}
1838

1939
function shouldUseCrossOrigin(appConfig: AppConfig): boolean {
2040
return (
@@ -33,13 +53,7 @@ export function getArtifactCache(
3353
if (CrossOriginStorage.isAvailable()) {
3454
return new CrossOriginStorageCache(scope);
3555
}
36-
// Fallback to Cache API
37-
if (!crossOriginUnavailableLogged) {
38-
logger(
39-
"Cross-origin storage backend requested but unavailable; falling back to Cache API.",
40-
);
41-
crossOriginUnavailableLogged = true;
42-
}
56+
scheduleCrossOriginFallbackWarning(logger);
4357
}
4458
if (backend === "indexeddb") {
4559
return new tvmjs.ArtifactIndexedDBCache(scope);
@@ -81,6 +95,10 @@ async function deleteTensorCacheEntries(
8195
try {
8296
manifest = await cache.fetchWithCache(jsonUrl, "json");
8397
} catch (err) {
98+
console.warn(
99+
`Failed to load tensor cache manifest at ${jsonUrl}; skipping deletion.`,
100+
err,
101+
);
84102
return;
85103
}
86104
const records = manifest?.records ?? [];

src/config.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ export interface ModelRecord {
269269
* passed to the load.
270270
*
271271
* @param model_list: models to be used.
272-
* @param useIndexedDBCache: if true, will use IndexedDBCache to cache models and other artifacts.
273-
* If false or unspecified, will use the Cache API. For more information of the two, see:
272+
* @param cacheBackend: the backend to use for caching models and other artifacts.
273+
* If unspecified, will use the Cache API. For more information, see:
274274
* https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#what_technologies_store_data_in_the_browser
275275
*
276276
* @note Note that the Cache API is more well-tested in WebLLM as of now.
@@ -279,15 +279,14 @@ export type CacheBackend = "cache" | "indexeddb" | "cross-origin";
279279

280280
export interface AppConfig {
281281
model_list: Array<ModelRecord>;
282-
useIndexedDBCache?: boolean;
283282
cacheBackend?: CacheBackend;
284283
}
285284

286285
export function getCacheBackend(appConfig: AppConfig): CacheBackend {
287286
if (appConfig.cacheBackend !== undefined) {
288287
return appConfig.cacheBackend;
289288
}
290-
return appConfig.useIndexedDBCache ? "indexeddb" : "cache";
289+
return "cache";
291290
}
292291

293292
/**
@@ -319,7 +318,7 @@ export const functionCallingModelIds = [
319318
* current WebLLM npm version.
320319
*/
321320
export const prebuiltAppConfig: AppConfig = {
322-
useIndexedDBCache: false,
321+
cacheBackend: "cache",
323322
model_list: [
324323
// Llama-3.2
325324
{

src/cross_origin_storage.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
const HASH_ALGORITHM = "SHA-256";
22
const HASH_MATCH_REGEX = /[A-Fa-f0-9]{64}/;
3+
const AVAILABILITY_POLL_INTERVAL_MS = 100;
4+
const DEFAULT_AVAILABILITY_TIMEOUT_MS = 3000;
5+
const HASH_CACHE_SYMBOL = Symbol.for("mlc.crossOriginStorage.hashCache");
6+
7+
const globalScope = globalThis as Record<PropertyKey, unknown>;
8+
if (!globalScope[HASH_CACHE_SYMBOL]) {
9+
globalScope[HASH_CACHE_SYMBOL] = new Map<string, CrossOriginHashDescriptor>();
10+
}
11+
const GLOBAL_HASH_CACHE = globalScope[HASH_CACHE_SYMBOL] as Map<
12+
string,
13+
CrossOriginHashDescriptor
14+
>;
315

416
export interface CrossOriginHashDescriptor {
517
algorithm: string;
@@ -16,7 +28,6 @@ interface CrossOriginStorageAPI {
1628
descriptors: CrossOriginHashDescriptor[],
1729
options?: { create?: boolean },
1830
): Promise<CrossOriginStorageHandle[]>;
19-
removeFileHandles?(descriptors: CrossOriginHashDescriptor[]): Promise<void>;
2031
}
2132

2233
type RequestLike = string | URL | Request | { url?: string };
@@ -25,13 +36,16 @@ declare global {
2536
interface Navigator {
2637
crossOriginStorage?: CrossOriginStorageAPI;
2738
}
39+
interface WorkerNavigator {
40+
crossOriginStorage?: CrossOriginStorageAPI;
41+
}
2842
}
2943

3044
export default class CrossOriginStorage {
3145
private hashCache: Map<string, CrossOriginHashDescriptor>;
3246

3347
constructor() {
34-
this.hashCache = new Map();
48+
this.hashCache = GLOBAL_HASH_CACHE;
3549
}
3650

3751
static isAvailable(): boolean {
@@ -42,6 +56,35 @@ export default class CrossOriginStorage {
4256
);
4357
}
4458

59+
static async waitForAvailability(
60+
timeoutMs: number = DEFAULT_AVAILABILITY_TIMEOUT_MS,
61+
): Promise<boolean> {
62+
if (CrossOriginStorage.isAvailable()) {
63+
return true;
64+
}
65+
if (typeof navigator === "undefined") {
66+
return false;
67+
}
68+
if (typeof setTimeout === "undefined") {
69+
return false;
70+
}
71+
return new Promise((resolve) => {
72+
const deadline = Date.now() + timeoutMs;
73+
const tick = () => {
74+
if (CrossOriginStorage.isAvailable()) {
75+
resolve(true);
76+
return;
77+
}
78+
if (Date.now() >= deadline) {
79+
resolve(false);
80+
return;
81+
}
82+
setTimeout(tick, AVAILABILITY_POLL_INTERVAL_MS);
83+
};
84+
setTimeout(tick, AVAILABILITY_POLL_INTERVAL_MS);
85+
});
86+
}
87+
4588
async match(request: RequestLike): Promise<Response | undefined> {
4689
const url = this.normalizeRequest(request);
4790
const hash = await this.resolveHashDescriptor(url);
@@ -84,17 +127,10 @@ export default class CrossOriginStorage {
84127
this.hashCache.set(url, hash);
85128
}
86129

130+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
87131
async delete(request: RequestLike): Promise<void> {
88-
const url = this.normalizeRequest(request);
89-
const hash = await this.resolveHashDescriptor(url);
90-
if (!hash) {
91-
return;
92-
}
93-
const api = this.getApi();
94-
if (api && typeof api.removeFileHandles === "function") {
95-
await api.removeFileHandles([hash]);
96-
}
97-
this.hashCache.delete(url);
132+
// Currently no delete API provided by Cross-Origin Storage Extension
133+
return;
98134
}
99135

100136
private getApi(): CrossOriginStorageAPI | undefined {
@@ -145,7 +181,7 @@ export default class CrossOriginStorage {
145181
if (metadataHash) {
146182
return metadataHash;
147183
}
148-
if (/\/resolve\/main\//.test(url)) {
184+
if (/\/resolve\//.test(url)) {
149185
const pointerHash = await this.extractHashFromPointer(url);
150186
if (pointerHash) {
151187
return pointerHash;

src/cross_origin_storage_cache.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ export class CrossOriginStorageCache implements tvmjs.ArtifactCacheTemplate {
6767
}
6868

6969
async deleteInCache(_url: string): Promise<void> {
70-
// no delete API currently provided by Cross-Origin Storage
71-
return;
70+
await this.storage.delete(_url);
7271
}
7372

7473
private async responseToStoreType(

src/utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,7 @@ export function areAppConfigsEqual(
7676
return config1 === config2;
7777
}
7878

79-
// Check if both configurations have the same IndexedDB cache usage
80-
if (config1.useIndexedDBCache !== config2.useIndexedDBCache) {
81-
return false;
82-
}
79+
// Check if both configurations have the same cache backend
8380
if (config1.cacheBackend !== config2.cacheBackend) {
8481
return false;
8582
}

tests/scripts/sanity_checks/sanity_checks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ async function testLogprobs(modelId: string, appConfig: webllm.AppConfig) {
157157
async function main() {
158158
const modelId = "Qwen3-0.6B-q0f32-MLC";
159159
const appConfig = webllm.prebuiltAppConfig;
160-
appConfig.useIndexedDBCache = true;
160+
appConfig.cacheBackend = "indexeddb";
161161
setLabel("gpu-test-label", "Running tests...");
162162
let passed = 0,
163163
total = 0;

0 commit comments

Comments
 (0)