midah commited on
Commit
a49e6fb
·
1 Parent(s): bed2f80

Fix: add utils/data files and update gitignore

Browse files
.dockerignore CHANGED
@@ -32,10 +32,9 @@ frontend/.env.development.local
32
  frontend/.env.test.local
33
  frontend/.env.production.local
34
 
35
- # Cache (will be created at runtime)
36
- cache/*.pkl
37
- cache/*.npy
38
- !cache/.gitkeep
39
 
40
  # Documentation
41
  *.md
 
32
  frontend/.env.test.local
33
  frontend/.env.production.local
34
 
35
+ # Keep cache files for fast startup (precomputed UMAP)
36
+ # cache/*.pkl # INCLUDED for HF Spaces deployment
37
+ # cache/*.npy # INCLUDED for HF Spaces deployment
 
38
 
39
  # Documentation
40
  *.md
.gitignore CHANGED
@@ -34,5 +34,6 @@ flagged/
34
  # Data
35
  *.parquet
36
  *.csv
37
- data/
 
38
 
 
34
  # Data
35
  *.parquet
36
  *.csv
37
+ /data/
38
+ !frontend/src/utils/data/
39
 
Dockerfile CHANGED
@@ -1,37 +1,33 @@
1
- # Use Python 3.11 slim image for smaller size
2
  FROM python:3.11-slim
3
 
4
- # Set working directory
 
5
  WORKDIR /app
6
 
7
- # Install system dependencies
8
- RUN apt-get update && apt-get install -y \
9
- gcc \
10
- g++ \
11
- && rm -rf /var/lib/apt/lists/*
12
 
13
- # Copy requirements first for better caching
14
  COPY requirements.txt .
15
- COPY backend/requirements.txt backend/requirements.txt
16
 
17
- # Install Python dependencies
18
- RUN pip install --no-cache-dir -r requirements.txt && \
19
- pip install --no-cache-dir -r backend/requirements.txt
20
 
21
- # Copy application code
22
- COPY . .
 
23
 
24
- # Create cache directory
25
- RUN mkdir -p cache
26
 
27
- # Expose port (will be overridden by PORT env var in cloud platforms)
28
- EXPOSE 8000
29
-
30
- # Set environment variables
31
  ENV PYTHONUNBUFFERED=1
32
- ENV PORT=8000
 
33
 
34
- # Run the application
35
  WORKDIR /app/backend
36
- CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
37
 
 
 
1
+ # Hugging Face Spaces Docker deployment
2
  FROM python:3.11-slim
3
 
4
+ # Create non-root user (required by HF Spaces)
5
+ RUN useradd -m -u 1000 user
6
  WORKDIR /app
7
 
8
+ # Install dependencies
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ gcc g++ && \
11
+ rm -rf /var/lib/apt/lists/*
 
12
 
 
13
  COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
 
16
+ # Copy application
17
+ COPY --chown=user backend/ /app/backend/
 
18
 
19
+ # Bundle precomputed data for instant startup
20
+ COPY --chown=user precomputed_data/ /app/precomputed_data/
21
+ COPY --chown=user cache/ /app/cache/
22
 
23
+ # Switch to non-root user
24
+ USER user
25
 
 
 
 
 
26
  ENV PYTHONUNBUFFERED=1
27
+ ENV PORT=7860
28
+ ENV ALLOW_ALL_ORIGINS=true
29
 
 
30
  WORKDIR /app/backend
31
+ EXPOSE 7860
32
 
33
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
frontend/src/utils/data/binaryFilter.ts ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Client-side filtering engine for binary dataset.
3
+ * Works with TypedArrays for fast vectorized operations.
4
+ */
5
+
6
+ export interface ModelData {
7
+ x: Float32Array;
8
+ y: Float32Array;
9
+ z: Float32Array;
10
+ domainId: Uint8Array;
11
+ licenseId: Uint8Array;
12
+ familyId: Uint16Array;
13
+ flags: Uint8Array;
14
+ modelIds: string[];
15
+ domains: string[];
16
+ licenses: string[];
17
+ }
18
+
19
+ export interface FilterCriteria {
20
+ domains?: string[];
21
+ licenses?: string[];
22
+ isBaseModel?: boolean | null;
23
+ families?: number[];
24
+ }
25
+
26
+ /**
27
+ * Create a boolean mask based on filter criteria.
28
+ * Returns Uint8Array where 1 = included, 0 = excluded.
29
+ */
30
+ export function createFilterMask(
31
+ data: ModelData,
32
+ criteria: FilterCriteria
33
+ ): Uint8Array {
34
+ const numModels = data.x.length;
35
+ const mask = new Uint8Array(numModels);
36
+
37
+ // Initialize all to 1 (included)
38
+ mask.fill(1);
39
+
40
+ // Filter by domain
41
+ if (criteria.domains && criteria.domains.length > 0) {
42
+ const domainSet = new Set(criteria.domains);
43
+ for (let i = 0; i < numModels; i++) {
44
+ const domain = data.domains[data.domainId[i]];
45
+ if (!domainSet.has(domain)) {
46
+ mask[i] = 0;
47
+ }
48
+ }
49
+ }
50
+
51
+ // Filter by license
52
+ if (criteria.licenses && criteria.licenses.length > 0) {
53
+ const licenseSet = new Set(criteria.licenses);
54
+ for (let i = 0; i < numModels; i++) {
55
+ const license = data.licenses[data.licenseId[i]];
56
+ if (!licenseSet.has(license)) {
57
+ mask[i] = 0;
58
+ }
59
+ }
60
+ }
61
+
62
+ // Filter by base model flag
63
+ if (criteria.isBaseModel !== undefined && criteria.isBaseModel !== null) {
64
+ for (let i = 0; i < numModels; i++) {
65
+ const isBase = (data.flags[i] & 0x01) !== 0;
66
+ if (isBase !== criteria.isBaseModel) {
67
+ mask[i] = 0;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Filter by family
73
+ if (criteria.families && criteria.families.length > 0) {
74
+ const familySet = new Set(criteria.families);
75
+ for (let i = 0; i < numModels; i++) {
76
+ if (!familySet.has(data.familyId[i])) {
77
+ mask[i] = 0;
78
+ }
79
+ }
80
+ }
81
+
82
+ return mask;
83
+ }
84
+
85
+ /**
86
+ * Apply filter mask to data and return filtered arrays.
87
+ */
88
+ export function applyMask(
89
+ data: ModelData,
90
+ mask: Uint8Array
91
+ ): {
92
+ x: Float32Array;
93
+ y: Float32Array;
94
+ z: Float32Array;
95
+ indices: number[];
96
+ count: number;
97
+ } {
98
+ const count = mask.reduce((sum, val) => sum + val, 0);
99
+ const x = new Float32Array(count);
100
+ const y = new Float32Array(count);
101
+ const z = new Float32Array(count);
102
+ const indices: number[] = [];
103
+
104
+ let j = 0;
105
+ for (let i = 0; i < mask.length; i++) {
106
+ if (mask[i] === 1) {
107
+ x[j] = data.x[i];
108
+ y[j] = data.y[i];
109
+ z[j] = data.z[i];
110
+ indices.push(i);
111
+ j++;
112
+ }
113
+ }
114
+
115
+ return { x, y, z, indices, count };
116
+ }
117
+
118
+ /**
119
+ * Get model ID for a given index.
120
+ */
121
+ export function getModelId(data: ModelData, index: number): string {
122
+ return data.modelIds[index] || '';
123
+ }
124
+
125
+ /**
126
+ * Get domain name for a given index.
127
+ */
128
+ export function getDomain(data: ModelData, index: number): string {
129
+ return data.domains[data.domainId[index]] || '';
130
+ }
131
+
132
+ /**
133
+ * Get license name for a given index.
134
+ */
135
+ export function getLicense(data: ModelData, index: number): string {
136
+ return data.licenses[data.licenseId[index]] || '';
137
+ }
138
+
139
+ /**
140
+ * Check if model is a base model (no parent).
141
+ */
142
+ export function isBaseModel(data: ModelData, index: number): boolean {
143
+ return (data.flags[index] & 0x01) !== 0;
144
+ }
145
+
146
+ /**
147
+ * Check if model has children.
148
+ */
149
+ export function hasChildren(data: ModelData, index: number): boolean {
150
+ return (data.flags[index] & 0x04) !== 0;
151
+ }
152
+
frontend/src/utils/data/enhancedCache.ts ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Enhanced IndexedDB cache with TTL and automatic cleanup.
3
+ * Uses idb library for better reliability and performance.
4
+ */
5
+ import { openDB, DBSchema, IDBPDatabase } from 'idb';
6
+
7
+ interface CacheEntry {
8
+ key: string;
9
+ data: any;
10
+ timestamp: number;
11
+ ttl: number;
12
+ size?: number;
13
+ }
14
+
15
+ interface CacheDB extends DBSchema {
16
+ cache: {
17
+ key: string;
18
+ value: CacheEntry;
19
+ indexes: { 'by-timestamp': number };
20
+ };
21
+ }
22
+
23
+ class EnhancedIndexedDBCache {
24
+ private db: IDBPDatabase<CacheDB> | null = null;
25
+ private dbName = 'hfviz-cache-v2';
26
+ private readonly storeName: 'cache' = 'cache';
27
+ private maxSize = 100 * 1024 * 1024; // 100MB
28
+ private defaultTTL = 5 * 60 * 1000; // 5 minutes
29
+
30
+ async init(): Promise<void> {
31
+ try {
32
+ this.db = await openDB<CacheDB>(this.dbName, 2, {
33
+ upgrade(db, oldVersion) {
34
+ if (!db.objectStoreNames.contains('cache')) {
35
+ const store = db.createObjectStore('cache', { keyPath: 'key' });
36
+ store.createIndex('by-timestamp', 'timestamp');
37
+ }
38
+ },
39
+ });
40
+
41
+ // Cleanup on init
42
+ await this.cleanup();
43
+ } catch (err) {
44
+ console.error('Failed to initialize IndexedDB:', err);
45
+ }
46
+ }
47
+
48
+ async get<T = any>(key: string): Promise<T | null> {
49
+ if (!this.db) await this.init();
50
+ if (!this.db) return null;
51
+
52
+ try {
53
+ const entry = await this.db.get(this.storeName, key);
54
+
55
+ if (!entry) return null;
56
+
57
+ // Check if expired
58
+ if (Date.now() - entry.timestamp > entry.ttl) {
59
+ await this.delete(key);
60
+ return null;
61
+ }
62
+
63
+ return entry.data as T;
64
+ } catch (err) {
65
+ console.error('IndexedDB get error:', err);
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async set(key: string, data: any, ttl: number = this.defaultTTL): Promise<boolean> {
71
+ if (!this.db) await this.init();
72
+ if (!this.db) return false;
73
+
74
+ try {
75
+ // Estimate size
76
+ const size = new Blob([JSON.stringify(data)]).size;
77
+
78
+ // Check if we need to make space
79
+ await this.ensureSpace(size);
80
+
81
+ const entry: CacheEntry = {
82
+ key,
83
+ data,
84
+ timestamp: Date.now(),
85
+ ttl,
86
+ size,
87
+ };
88
+
89
+ await this.db.put(this.storeName, entry);
90
+ return true;
91
+ } catch (err) {
92
+ console.error('IndexedDB set error:', err);
93
+ return false;
94
+ }
95
+ }
96
+
97
+ async delete(key: string): Promise<boolean> {
98
+ if (!this.db) await this.init();
99
+ if (!this.db) return false;
100
+
101
+ try {
102
+ await this.db.delete(this.storeName, key);
103
+ return true;
104
+ } catch (err) {
105
+ console.error('IndexedDB delete error:', err);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ async clear(): Promise<boolean> {
111
+ if (!this.db) await this.init();
112
+ if (!this.db) return false;
113
+
114
+ try {
115
+ await this.db.clear(this.storeName);
116
+ return true;
117
+ } catch (err) {
118
+ console.error('IndexedDB clear error:', err);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ async cleanup(): Promise<void> {
124
+ if (!this.db) return;
125
+
126
+ try {
127
+ const tx = this.db.transaction(this.storeName, 'readwrite');
128
+ const store = tx.objectStore(this.storeName);
129
+ const now = Date.now();
130
+
131
+ let cursor = await store.openCursor();
132
+ while (cursor) {
133
+ const entry = cursor.value;
134
+ if (now - entry.timestamp > entry.ttl) {
135
+ await cursor.delete();
136
+ }
137
+ cursor = await cursor.continue();
138
+ }
139
+
140
+ await tx.done;
141
+ } catch (err) {
142
+ console.error('IndexedDB cleanup error:', err);
143
+ }
144
+ }
145
+
146
+ async ensureSpace(requiredSize: number): Promise<void> {
147
+ if (!this.db) return;
148
+
149
+ try {
150
+ // Get all entries sorted by timestamp
151
+ const tx = this.db.transaction(this.storeName, 'readwrite');
152
+ const index = tx.objectStore(this.storeName).index('by-timestamp');
153
+ const entries: CacheEntry[] = [];
154
+
155
+ let cursor = await index.openCursor();
156
+ while (cursor) {
157
+ entries.push(cursor.value);
158
+ cursor = await cursor.continue();
159
+ }
160
+
161
+ await tx.done;
162
+
163
+ // Calculate total size
164
+ let totalSize = entries.reduce((sum, e) => sum + (e.size || 0), 0);
165
+
166
+ // Remove oldest entries if needed
167
+ if (totalSize + requiredSize > this.maxSize) {
168
+ const sorted = entries.sort((a, b) => a.timestamp - b.timestamp);
169
+
170
+ for (const entry of sorted) {
171
+ if (totalSize + requiredSize <= this.maxSize) break;
172
+ await this.delete(entry.key);
173
+ totalSize -= (entry.size || 0);
174
+ }
175
+ }
176
+ } catch (err) {
177
+ console.error('IndexedDB ensureSpace error:', err);
178
+ }
179
+ }
180
+
181
+ async stats(): Promise<{ count: number; totalSize: number }> {
182
+ if (!this.db) await this.init();
183
+ if (!this.db) return { count: 0, totalSize: 0 };
184
+
185
+ try {
186
+ const tx = this.db.transaction(this.storeName, 'readonly');
187
+ const store = tx.objectStore(this.storeName);
188
+
189
+ let count = 0;
190
+ let totalSize = 0;
191
+
192
+ let cursor = await store.openCursor();
193
+ while (cursor) {
194
+ count++;
195
+ totalSize += cursor.value.size || 0;
196
+ cursor = await cursor.continue();
197
+ }
198
+
199
+ await tx.done;
200
+ return { count, totalSize };
201
+ } catch (err) {
202
+ console.error('IndexedDB stats error:', err);
203
+ return { count: 0, totalSize: 0 };
204
+ }
205
+ }
206
+ }
207
+
208
+ // Export singleton
209
+ export const enhancedCache = new EnhancedIndexedDBCache();
210
+
211
+ // Auto-cleanup every 5 minutes
212
+ if (typeof window !== 'undefined') {
213
+ setInterval(() => {
214
+ enhancedCache.cleanup();
215
+ }, 5 * 60 * 1000);
216
+ }
217
+
218
+ export default enhancedCache;
219
+
220
+
frontend/src/utils/data/indexedDB.ts ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * IndexedDB utility for client-side caching of embeddings and model data.
3
+ * Enables offline access and faster subsequent loads.
4
+ */
5
+
6
+ const DB_NAME = 'hf_viz_cache';
7
+ const DB_VERSION = 1;
8
+
9
+ interface CacheEntry<T> {
10
+ key: string;
11
+ data: T;
12
+ timestamp: number;
13
+ version: string;
14
+ }
15
+
16
+ const CACHE_VERSION = '1.0.0'; // Increment to invalidate old cache
17
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
18
+ const MAX_CACHE_SIZE = 100; // Maximum number of entries per store
19
+
20
+ class IndexedDBCache {
21
+ private db: IDBDatabase | null = null;
22
+ private initPromise: Promise<void> | null = null;
23
+
24
+ private async init(): Promise<void> {
25
+ if (this.db) return;
26
+ if (this.initPromise) return this.initPromise;
27
+
28
+ this.initPromise = new Promise((resolve, reject) => {
29
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
30
+
31
+ request.onerror = () => reject(request.error);
32
+ request.onsuccess = () => {
33
+ this.db = request.result;
34
+ resolve();
35
+ };
36
+
37
+ request.onupgradeneeded = (event) => {
38
+ const db = (event.target as IDBOpenDBRequest).result;
39
+
40
+ // Create object stores
41
+ if (!db.objectStoreNames.contains('embeddings')) {
42
+ db.createObjectStore('embeddings', { keyPath: 'key' });
43
+ }
44
+ if (!db.objectStoreNames.contains('reduced_embeddings')) {
45
+ db.createObjectStore('reduced_embeddings', { keyPath: 'key' });
46
+ }
47
+ if (!db.objectStoreNames.contains('models')) {
48
+ db.createObjectStore('models', { keyPath: 'key' });
49
+ }
50
+ if (!db.objectStoreNames.contains('stats')) {
51
+ db.createObjectStore('stats', { keyPath: 'key' });
52
+ }
53
+ };
54
+ });
55
+
56
+ return this.initPromise;
57
+ }
58
+
59
+ private async getStore(storeName: string, mode: 'readonly' | 'readwrite' = 'readonly'): Promise<IDBObjectStore> {
60
+ await this.init();
61
+ if (!this.db) throw new Error('Database not initialized');
62
+
63
+ const transaction = this.db.transaction([storeName], mode);
64
+ return transaction.objectStore(storeName);
65
+ }
66
+
67
+ async set<T>(storeName: string, key: string, data: T): Promise<void> {
68
+ await this.init();
69
+ if (!this.db) throw new Error('Database not initialized');
70
+
71
+ // Prepare entry data
72
+ const entry: CacheEntry<T> = {
73
+ key,
74
+ data,
75
+ timestamp: Date.now(),
76
+ version: CACHE_VERSION,
77
+ };
78
+
79
+ // Create a single transaction for all operations
80
+ const transaction = this.db.transaction([storeName], 'readwrite');
81
+ const store = transaction.objectStore(storeName);
82
+
83
+ // Queue the put operation FIRST to ensure transaction stays alive
84
+ // This is critical - by queueing it immediately, we ensure the transaction
85
+ // has a pending request throughout all other operations
86
+ const putPromise = new Promise<void>((resolve, reject) => {
87
+ const request = store.put(entry);
88
+ request.onsuccess = () => resolve();
89
+ request.onerror = () => reject(request.error);
90
+ });
91
+
92
+ // Enforce cache size limit - delete oldest entries if at limit
93
+ const count = await new Promise<number>((resolve, reject) => {
94
+ const countRequest = store.count();
95
+ countRequest.onsuccess = () => resolve(countRequest.result);
96
+ countRequest.onerror = () => reject(countRequest.error);
97
+ });
98
+
99
+ // Queue delete operations if needed
100
+ const deletePromises: Promise<void>[] = [];
101
+
102
+ if (count >= MAX_CACHE_SIZE) {
103
+ // Get all entries, sort by timestamp, delete oldest
104
+ const allEntries: Array<{ key: string; timestamp: number }> = [];
105
+ const getAllRequest = store.openCursor();
106
+
107
+ await new Promise<void>((resolve, reject) => {
108
+ getAllRequest.onsuccess = (event: any) => {
109
+ const cursor = event.target.result;
110
+ if (cursor) {
111
+ const entry = cursor.value as CacheEntry<T>;
112
+ allEntries.push({ key: entry.key, timestamp: entry.timestamp });
113
+ cursor.continue();
114
+ } else {
115
+ resolve();
116
+ }
117
+ };
118
+ getAllRequest.onerror = () => reject(getAllRequest.error);
119
+ });
120
+
121
+ // Sort by timestamp (oldest first) and delete excess
122
+ allEntries.sort((a, b) => a.timestamp - b.timestamp);
123
+ const toDelete = allEntries.slice(0, count - MAX_CACHE_SIZE + 1);
124
+
125
+ // Queue all delete operations immediately
126
+ for (const entryToDelete of toDelete) {
127
+ deletePromises.push(
128
+ new Promise<void>((resolve, reject) => {
129
+ const deleteRequest = store.delete(entryToDelete.key);
130
+ deleteRequest.onsuccess = () => resolve();
131
+ deleteRequest.onerror = () => reject(deleteRequest.error);
132
+ })
133
+ );
134
+ }
135
+ }
136
+
137
+ // Wait for both deletes (if any) and put to complete
138
+ // The put request was queued first, so transaction stays alive throughout
139
+ await Promise.all([...deletePromises, putPromise]);
140
+ }
141
+
142
+ async get<T>(storeName: string, key: string): Promise<T | null> {
143
+ const store = await this.getStore(storeName);
144
+
145
+ return new Promise((resolve, reject) => {
146
+ const request = store.get(key);
147
+ request.onsuccess = async () => {
148
+ const entry = request.result as CacheEntry<T> | undefined;
149
+ if (!entry) {
150
+ resolve(null);
151
+ return;
152
+ }
153
+
154
+ // Check version
155
+ if (entry.version !== CACHE_VERSION) {
156
+ // Version mismatch, delete old entry using readwrite transaction
157
+ try {
158
+ await this.delete(storeName, key);
159
+ } catch (err) {
160
+ // Ignore delete errors
161
+ }
162
+ resolve(null);
163
+ return;
164
+ }
165
+
166
+ // Check TTL
167
+ const age = Date.now() - entry.timestamp;
168
+ if (age > CACHE_TTL_MS) {
169
+ // Cache expired, delete old entry using readwrite transaction
170
+ try {
171
+ await this.delete(storeName, key);
172
+ } catch (err) {
173
+ // Ignore delete errors
174
+ }
175
+ resolve(null);
176
+ return;
177
+ }
178
+
179
+ resolve(entry.data);
180
+ };
181
+ request.onerror = () => reject(request.error);
182
+ });
183
+ }
184
+
185
+ async has(storeName: string, key: string): Promise<boolean> {
186
+ const store = await this.getStore(storeName);
187
+
188
+ return new Promise((resolve, reject) => {
189
+ const request = store.getKey(key);
190
+ request.onsuccess = () => resolve(request.result !== undefined);
191
+ request.onerror = () => reject(request.error);
192
+ });
193
+ }
194
+
195
+ async delete(storeName: string, key: string): Promise<void> {
196
+ const store = await this.getStore(storeName, 'readwrite');
197
+
198
+ return new Promise((resolve, reject) => {
199
+ const request = store.delete(key);
200
+ request.onsuccess = () => resolve();
201
+ request.onerror = () => reject(request.error);
202
+ });
203
+ }
204
+
205
+ async clear(storeName: string): Promise<void> {
206
+ const store = await this.getStore(storeName, 'readwrite');
207
+
208
+ return new Promise((resolve, reject) => {
209
+ const request = store.clear();
210
+ request.onsuccess = () => resolve();
211
+ request.onerror = () => reject(request.error);
212
+ });
213
+ }
214
+
215
+ async getCacheSize(storeName: string): Promise<number> {
216
+ const store = await this.getStore(storeName);
217
+
218
+ return new Promise((resolve, reject) => {
219
+ const request = store.count();
220
+ request.onsuccess = () => resolve(request.result);
221
+ request.onerror = () => reject(request.error);
222
+ });
223
+ }
224
+
225
+ // Helper methods for specific data types
226
+ async cacheModels(key: string, models: any[]): Promise<void> {
227
+ return this.set('models', key, models);
228
+ }
229
+
230
+ async getCachedModels(key: string): Promise<any[] | null> {
231
+ return this.get<any[]>('models', key);
232
+ }
233
+
234
+ async cacheStats(key: string, stats: any): Promise<void> {
235
+ return this.set('stats', key, stats);
236
+ }
237
+
238
+ async getCachedStats(key: string): Promise<any | null> {
239
+ return this.get<any>('stats', key);
240
+ }
241
+
242
+ // Generate cache key from filter parameters
243
+ static generateCacheKey(params: {
244
+ minDownloads: number;
245
+ minLikes: number;
246
+ searchQuery?: string;
247
+ projectionMethod: string;
248
+ }): string {
249
+ return JSON.stringify({
250
+ minDownloads: params.minDownloads,
251
+ minLikes: params.minLikes,
252
+ searchQuery: params.searchQuery || '',
253
+ projectionMethod: params.projectionMethod,
254
+ });
255
+ }
256
+ }
257
+
258
+ // Export singleton instance
259
+ export const cache = new IndexedDBCache();
260
+ export { IndexedDBCache };
261
+ export default cache;
262
+
netlify.toml CHANGED
@@ -5,8 +5,6 @@
5
 
6
  [build.environment]
7
  NODE_VERSION = "18"
8
- # Set your Railway backend URL in Netlify dashboard:
9
- # Site settings → Environment variables → REACT_APP_API_URL
10
 
11
  [[redirects]]
12
  from = "/*"
@@ -21,9 +19,7 @@
21
  X-Content-Type-Options = "nosniff"
22
  Referrer-Policy = "strict-origin-when-cross-origin"
23
 
24
- # Cache static assets aggressively
25
  [[headers]]
26
  for = "/static/*"
27
  [headers.values]
28
  Cache-Control = "public, max-age=31536000, immutable"
29
-
 
5
 
6
  [build.environment]
7
  NODE_VERSION = "18"
 
 
8
 
9
  [[redirects]]
10
  from = "/*"
 
19
  X-Content-Type-Options = "nosniff"
20
  Referrer-Policy = "strict-origin-when-cross-origin"
21
 
 
22
  [[headers]]
23
  for = "/static/*"
24
  [headers.values]
25
  Cache-Control = "public, max-age=31536000, immutable"