Skip to content

Commit 794b474

Browse files
vchaindzclaude
andcommitted
feat: Implement JSONIC v2 progressive loading system with hybrid wrapper
- Add feature detection system with environment capability checking - Implement hybrid loader with on-demand feature loading and code splitting - Add Service Worker for offline-first caching and progressive enhancement - Integrate debug tools and performance monitoring features - Add server sync client for real-time bi-directional synchronization - Fix Web Worker compatibility issues (window reference errors) - Update Vite config for optimal code splitting and chunk organization - Fix favicon paths for GitHub Pages deployment - Add comprehensive error handling and fallback strategies Key improvements: - Smaller initial bundle size through progressive loading - Better performance with intelligent caching strategies - Enhanced developer experience with debug tools - Production-ready offline support - Zero-config setup with smart defaults 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent dc9f794 commit 794b474

File tree

14 files changed

+2380
-20
lines changed

14 files changed

+2380
-20
lines changed

public/data/database.jsonic

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/data/database.jsonic.gz

3 Bytes
Binary file not shown.

public/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
"description": "Real-time AI model performance monitoring and comparison dashboard",
55
"icons": [
66
{
7-
"src": "/favicon.svg",
7+
"src": "/agentx-benchmark-ui/favicon.svg",
88
"sizes": "any",
99
"type": "image/svg+xml"
1010
},
1111
{
12-
"src": "/agentx-logo.svg",
12+
"src": "/agentx-benchmark-ui/agentx-logo.svg",
1313
"sizes": "any",
1414
"type": "image/svg+xml",
1515
"purpose": "maskable"
1616
}
1717
],
18-
"start_url": "/",
18+
"start_url": "/agentx-benchmark-ui/",
1919
"display": "standalone",
2020
"theme_color": "#2563eb",
2121
"background_color": "#ffffff"

public/service-worker.js

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/**
2+
* JSONIC Service Worker
3+
* Provides offline-first caching with progressive loading
4+
*/
5+
6+
const CACHE_VERSION = 'jsonic-v1.0.0';
7+
const CORE_CACHE = `${CACHE_VERSION}-core`;
8+
const FEATURE_CACHE = `${CACHE_VERSION}-features`;
9+
const DATA_CACHE = `${CACHE_VERSION}-data`;
10+
const STATIC_CACHE = `${CACHE_VERSION}-static`;
11+
12+
// Core files that should always be cached
13+
const CORE_FILES = [
14+
'/',
15+
'/index.html',
16+
'/manifest.json',
17+
'/favicon.ico',
18+
'/static/js/bundle.js',
19+
'/static/css/main.css'
20+
];
21+
22+
// Feature chunks that can be cached on-demand
23+
const FEATURE_PATTERNS = [
24+
/\/static\/js\/jsonic-.*\.chunk\.js$/,
25+
/\/static\/js\/\d+\.\w+\.chunk\.js$/,
26+
];
27+
28+
// API endpoints to cache
29+
const API_PATTERNS = [
30+
/\/api\/benchmarks\//,
31+
/\/api\/config\//,
32+
];
33+
34+
// Install event - pre-cache core files
35+
self.addEventListener('install', (event) => {
36+
console.log('[Service Worker] Installing...');
37+
38+
event.waitUntil(
39+
caches.open(CORE_CACHE).then((cache) => {
40+
console.log('[Service Worker] Pre-caching core files');
41+
return cache.addAll(CORE_FILES.filter(file => {
42+
// Only cache files that exist
43+
return fetch(file, { method: 'HEAD' })
44+
.then(() => true)
45+
.catch(() => false);
46+
}));
47+
}).then(() => {
48+
// Skip waiting to activate immediately
49+
return self.skipWaiting();
50+
})
51+
);
52+
});
53+
54+
// Activate event - clean up old caches
55+
self.addEventListener('activate', (event) => {
56+
console.log('[Service Worker] Activating...');
57+
58+
event.waitUntil(
59+
caches.keys().then((cacheNames) => {
60+
return Promise.all(
61+
cacheNames.map((cacheName) => {
62+
// Delete old version caches
63+
if (cacheName.startsWith('jsonic-') &&
64+
!cacheName.startsWith(CACHE_VERSION)) {
65+
console.log('[Service Worker] Deleting old cache:', cacheName);
66+
return caches.delete(cacheName);
67+
}
68+
})
69+
);
70+
}).then(() => {
71+
// Take control of all clients immediately
72+
return self.clients.claim();
73+
})
74+
);
75+
});
76+
77+
// Fetch event - serve from cache or network
78+
self.addEventListener('fetch', (event) => {
79+
const { request } = event;
80+
const url = new URL(request.url);
81+
82+
// Skip non-GET requests
83+
if (request.method !== 'GET') {
84+
return;
85+
}
86+
87+
// Skip chrome-extension and other non-http(s) protocols
88+
if (!request.url.startsWith('http')) {
89+
return;
90+
}
91+
92+
event.respondWith(handleFetch(request, url));
93+
});
94+
95+
/**
96+
* Handle fetch requests with appropriate caching strategy
97+
*/
98+
async function handleFetch(request, url) {
99+
// Check if it's a core file
100+
if (CORE_FILES.includes(url.pathname)) {
101+
return cacheFirst(request, CORE_CACHE);
102+
}
103+
104+
// Check if it's a feature chunk
105+
if (FEATURE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
106+
return cacheFirst(request, FEATURE_CACHE);
107+
}
108+
109+
// Check if it's an API endpoint
110+
if (API_PATTERNS.some(pattern => pattern.test(url.pathname))) {
111+
return networkFirst(request, DATA_CACHE);
112+
}
113+
114+
// Check if it's a static asset
115+
if (url.pathname.startsWith('/static/') ||
116+
url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$/)) {
117+
return cacheFirst(request, STATIC_CACHE);
118+
}
119+
120+
// Default: network first for HTML and other content
121+
return networkFirst(request, STATIC_CACHE);
122+
}
123+
124+
/**
125+
* Cache-first strategy
126+
* Try cache first, fallback to network
127+
*/
128+
async function cacheFirst(request, cacheName) {
129+
const cache = await caches.open(cacheName);
130+
131+
// Try to get from cache
132+
const cachedResponse = await cache.match(request);
133+
if (cachedResponse) {
134+
// Update cache in background
135+
fetchAndCache(request, cache);
136+
return cachedResponse;
137+
}
138+
139+
// Not in cache, fetch from network
140+
try {
141+
const networkResponse = await fetch(request);
142+
143+
// Cache successful responses
144+
if (networkResponse.ok) {
145+
cache.put(request, networkResponse.clone());
146+
}
147+
148+
return networkResponse;
149+
} catch (error) {
150+
console.error('[Service Worker] Fetch failed:', error);
151+
152+
// Return offline page if available
153+
const offlineResponse = await cache.match('/offline.html');
154+
if (offlineResponse) {
155+
return offlineResponse;
156+
}
157+
158+
throw error;
159+
}
160+
}
161+
162+
/**
163+
* Network-first strategy
164+
* Try network first, fallback to cache
165+
*/
166+
async function networkFirst(request, cacheName) {
167+
const cache = await caches.open(cacheName);
168+
169+
try {
170+
const networkResponse = await fetch(request);
171+
172+
// Cache successful responses
173+
if (networkResponse.ok) {
174+
cache.put(request, networkResponse.clone());
175+
}
176+
177+
return networkResponse;
178+
} catch (error) {
179+
// Network failed, try cache
180+
const cachedResponse = await cache.match(request);
181+
if (cachedResponse) {
182+
console.log('[Service Worker] Serving from cache:', request.url);
183+
return cachedResponse;
184+
}
185+
186+
// No cache available
187+
console.error('[Service Worker] Network and cache failed:', error);
188+
throw error;
189+
}
190+
}
191+
192+
/**
193+
* Fetch and update cache in background
194+
*/
195+
async function fetchAndCache(request, cache) {
196+
try {
197+
const response = await fetch(request);
198+
if (response.ok) {
199+
cache.put(request, response);
200+
}
201+
} catch (error) {
202+
// Silent fail - we already returned from cache
203+
}
204+
}
205+
206+
// Message handling for feature pre-caching
207+
self.addEventListener('message', (event) => {
208+
const { type, data } = event.data;
209+
210+
switch (type) {
211+
case 'PRE_CACHE_FEATURE':
212+
preCacheFeature(data.feature);
213+
break;
214+
215+
case 'GET_CACHE_STATUS':
216+
getCacheStatus().then(status => {
217+
event.ports[0].postMessage(status);
218+
});
219+
break;
220+
221+
case 'CLEAR_CACHE':
222+
clearAllCaches();
223+
break;
224+
225+
case 'SKIP_WAITING':
226+
self.skipWaiting();
227+
break;
228+
}
229+
});
230+
231+
/**
232+
* Pre-cache a feature module
233+
*/
234+
async function preCacheFeature(feature) {
235+
const cache = await caches.open(FEATURE_CACHE);
236+
const featureUrl = `/static/js/jsonic-${feature}.chunk.js`;
237+
238+
try {
239+
const response = await fetch(featureUrl);
240+
if (response.ok) {
241+
await cache.put(featureUrl, response);
242+
console.log(`[Service Worker] Pre-cached feature: ${feature}`);
243+
}
244+
} catch (error) {
245+
console.error(`[Service Worker] Failed to pre-cache feature ${feature}:`, error);
246+
}
247+
}
248+
249+
/**
250+
* Get cache statistics
251+
*/
252+
async function getCacheStatus() {
253+
const cacheNames = await caches.keys();
254+
const status = {};
255+
256+
for (const name of cacheNames) {
257+
const cache = await caches.open(name);
258+
const requests = await cache.keys();
259+
260+
status[name] = {
261+
count: requests.length,
262+
size: 0, // Size calculation would require iteration
263+
urls: requests.map(r => r.url)
264+
};
265+
}
266+
267+
return {
268+
version: CACHE_VERSION,
269+
caches: status,
270+
totalCaches: cacheNames.length
271+
};
272+
}
273+
274+
/**
275+
* Clear all caches
276+
*/
277+
async function clearAllCaches() {
278+
const cacheNames = await caches.keys();
279+
280+
await Promise.all(
281+
cacheNames.map(name => caches.delete(name))
282+
);
283+
284+
console.log('[Service Worker] All caches cleared');
285+
}
286+
287+
// Periodic cache cleanup (every hour)
288+
setInterval(() => {
289+
cleanupOldCaches();
290+
}, 60 * 60 * 1000);
291+
292+
/**
293+
* Clean up old cache entries
294+
*/
295+
async function cleanupOldCaches() {
296+
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
297+
const now = Date.now();
298+
299+
const cacheNames = await caches.keys();
300+
301+
for (const cacheName of cacheNames) {
302+
if (cacheName === CORE_CACHE) {
303+
continue; // Don't clean core cache
304+
}
305+
306+
const cache = await caches.open(cacheName);
307+
const requests = await cache.keys();
308+
309+
for (const request of requests) {
310+
const response = await cache.match(request);
311+
if (response) {
312+
const dateHeader = response.headers.get('date');
313+
if (dateHeader) {
314+
const responseTime = new Date(dateHeader).getTime();
315+
if (now - responseTime > maxAge) {
316+
await cache.delete(request);
317+
console.log('[Service Worker] Deleted old cache entry:', request.url);
318+
}
319+
}
320+
}
321+
}
322+
}
323+
}
324+
325+
console.log('[Service Worker] Loaded successfully');

src/components/JsonicDebugPanel.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,17 @@ export const JsonicDebugPanel: React.FC<DebugPanelProps> = ({ isOpen = false, on
201201
Clear
202202
</button>
203203
</div>
204-
{debugInfo?.slowQueries?.length > 0 ? (
204+
{debugInfo?.slowQueries && debugInfo.slowQueries.length > 0 ? (
205205
<div className="space-y-2">
206-
{debugInfo.slowQueries.slice(0, 5).map((query, i) => (
206+
{debugInfo?.slowQueries?.slice(0, 5).map((query, i) => (
207207
<div key={i} className="bg-gray-800 rounded p-2">
208208
<div className="flex justify-between mb-1">
209209
<span className="text-blue-400">{query.operation}</span>
210210
<span className="text-red-400">{query.duration.toFixed(2)}ms</span>
211211
</div>
212212
{query.details && (
213213
<div className="text-gray-500 text-xs truncate">
214-
{JSON.stringify(query.details)}
214+
{typeof query.details === 'string' ? query.details : JSON.stringify(query.details)}
215215
</div>
216216
)}
217217
</div>
@@ -277,9 +277,9 @@ export const JsonicDebugPanel: React.FC<DebugPanelProps> = ({ isOpen = false, on
277277
{activeTab === 'indexes' && (
278278
<div className="space-y-3">
279279
<h4 className="text-gray-400 mb-2">Active Indexes</h4>
280-
{debugInfo?.indexes?.length > 0 ? (
280+
{debugInfo?.indexes && debugInfo.indexes.length > 0 ? (
281281
<div className="space-y-2">
282-
{debugInfo.indexes.map((index) => (
282+
{debugInfo?.indexes?.map((index) => (
283283
<div key={index.field} className="bg-gray-800 rounded p-2">
284284
<div className="flex justify-between mb-1">
285285
<span className="text-blue-400">{index.field}</span>

src/components/PerformanceMonitor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useEffect, useState } from 'react';
2-
import { jsonicService } from '../services/jsonicService';
32

43
interface PerformanceTiming {
54
phase: string;

0 commit comments

Comments
 (0)