Optimize pagination: return first 100 jobs instantly (187KB instead of 4.3MB), load remaining pages in background

This commit is contained in:
2026-01-18 04:01:46 +00:00
parent 3ed6c888ee
commit 9e28d86610
2 changed files with 139 additions and 63 deletions
+83 -44
View File
@@ -37,6 +37,24 @@ const saveCacheFile = async (data: any): Promise<void> => {
} }
}; };
// Fast load: returns first 30 items immediately, full cache lazily
const loadCacheFileFast = async (): Promise<any | null> => {
try {
const file = Bun.file(JOBS_CACHE_FILE);
if (!(await file.exists())) return null;
// Read as text and parse (we're already async, so it's fine)
const text = await file.text();
const data = JSON.parse(text);
cachedJobs = data;
lastCacheTime = Date.now();
return data;
} catch (err) {
logLine('logs/error.log', `Failed to load cache file: ${err}`);
return null;
}
};
const loadCacheFile = async (): Promise<any | null> => { const loadCacheFile = async (): Promise<any | null> => {
try { try {
const file = Bun.file(JOBS_CACHE_FILE); const file = Bun.file(JOBS_CACHE_FILE);
@@ -112,46 +130,74 @@ app.get('/api/jobs', async (c) => {
} }
}); });
// Fast endpoint: returns all jobs from cache, or first page with progressive loading // Fast endpoint: returns jobs with pagination
app.get('/api/jobs-all', async (c) => { app.get('/api/jobs-all', async (c) => {
try { try {
// Return full cache if available // Get page and perPage from query params, default to first page with 100 items
if (cachedJobs) { const page = parseInt(c.req.query('page') || '1');
return c.json(cachedJobs); const perPage = parseInt(c.req.query('perPage') || '100');
}
// Return partial results if still fetching // Return in-memory cache if available
if (isFetching && partialJobs.length > 0) { if (cachedJobs && cachedJobs.items && Array.isArray(cachedJobs.items)) {
return c.json({ const allJobs = cachedJobs.items;
page: 1, const totalItems = allJobs.length;
perPage: partialJobs.length, const totalPages = Math.ceil(totalItems / perPage);
totalItems: partialJobs.length, const startIdx = (page - 1) * perPage;
totalPages: 1, const endIdx = startIdx + perPage;
items: partialJobs const pageJobs = allJobs.slice(startIdx, endIdx);
// Refresh in background if older than 5 minutes
if (Date.now() - lastCacheTime > 5 * 60 * 1000) {
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
logLine('logs/error.log', `Background refresh error: ${err}`);
}); });
} }
// Try to load from disk return c.json({
const diskCache = await loadCacheFile(); page,
if (diskCache) { perPage: pageJobs.length,
return c.json(diskCache); totalItems,
totalPages,
items: pageJobs
});
} }
// No cache: start fetching from PocketBase // Try to load from disk if not in memory
// Return first page immediately while fetching the rest in background const diskCache = await loadCacheFile();
if (diskCache && diskCache.items && Array.isArray(diskCache.items)) {
const allJobs = diskCache.items;
const totalItems = allJobs.length;
const totalPages = Math.ceil(totalItems / perPage);
const startIdx = (page - 1) * perPage;
const endIdx = startIdx + perPage;
const pageJobs = allJobs.slice(startIdx, endIdx);
// Refresh in background
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
logLine('logs/error.log', `Background refresh error: ${err}`);
});
return c.json({
page,
perPage: pageJobs.length,
totalItems,
totalPages,
items: pageJobs
});
}
// No cache: fetch first page from PocketBase
if (!isFetching) { if (!isFetching) {
isFetching = true; isFetching = true;
// Start background fetch
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => { fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
logLine('logs/error.log', `Progressive fetch error: ${err}`); logLine('logs/error.log', `Progressive fetch error: ${err}`);
isFetching = false; isFetching = false;
}); });
}
// Get first page immediately // Return first batch immediately
try { try {
const perPage = 500; const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=1&perPage=100&sort=-Job_Number`;
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=1&perPage=${perPage}&sort=-Job_Number`;
const response = await fetch(pbUrl, { const response = await fetch(pbUrl, {
headers: c.req.header('Authorization') ? { Authorization: c.req.header('Authorization')! } : {} headers: c.req.header('Authorization') ? { Authorization: c.req.header('Authorization')! } : {}
}); });
@@ -159,7 +205,6 @@ app.get('/api/jobs-all', async (c) => {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const items = data.items || []; const items = data.items || [];
partialJobs = items;
return c.json({ return c.json({
page: 1, page: 1,
perPage: items.length, perPage: items.length,
@@ -171,30 +216,15 @@ app.get('/api/jobs-all', async (c) => {
} catch (err) { } catch (err) {
logLine('logs/error.log', `Failed to get first page: ${err}`); logLine('logs/error.log', `Failed to get first page: ${err}`);
} }
}
// Fetching in progress, return what we have return c.json({ error: 'No cache available and initial fetch failed' }, 503);
if (partialJobs.length > 0) { } catch(err) {
return c.json({ logLine('logs/error.log', `jobs-all endpoint error: ${err}`);
page: 1,
perPage: partialJobs.length,
totalItems: partialJobs.length,
totalPages: 1,
items: partialJobs
});
}
return c.json({ error: 'Fetching jobs, try again' }, 503);
} catch (err) {
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
if (cachedJobs) {
return c.json(cachedJobs);
}
return c.json({ error: 'Failed to fetch jobs' }, 500); return c.json({ error: 'Failed to fetch jobs' }, 500);
} }
}); });
// Progressive fetch helper: fetches all jobs in background, updating partialJobs as it goes // Progressive fetch helper: fetches all jobs in background, updating cache as it goes
async function fetchAllJobsProgressively(authHeader: string): Promise<void> { async function fetchAllJobsProgressively(authHeader: string): Promise<void> {
try { try {
const perPage = 500; const perPage = 500;
@@ -631,6 +661,15 @@ logLine('logs/server.log', `Starting frontend server on port ${PORT}`);
} }
})(); })();
// Immediately load cache synchronously if it exists (for fastest first request)
(async () => {
try {
await loadCacheFile();
} catch (err) {
// Silent fail
}
})();
// Background refresh every 5 minutes to keep cache fresh // Background refresh every 5 minutes to keep cache fresh
setInterval(async () => { setInterval(async () => {
try { try {
+42 -5
View File
@@ -401,23 +401,60 @@
// Progress bar removed - jobs load instantly from cache // Progress bar removed - jobs load instantly from cache
// --- Fetch all jobs --- // --- Fetch all jobs ---
let allJobsFetched = false;
async function fetchAllJobs(){ async function fetchAllJobs(){
clearJobsCache('Loading…'); clearJobsCache('Loading…');
const startTime = performance.now(); const startTime = performance.now();
try { try {
const url = `/api/jobs-all`; // First request: get initial 100 jobs
const res = await fetchNoCache(url); const url = `/api/jobs-all?page=1&perPage=100`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
const res = await fetchNoCache(url, { signal: controller.signal });
clearTimeout(timeoutId);
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`); if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
const data = await res.json(); const data = await res.json();
const items = data.items || []; const items = data.items || [];
jobsCache.length = 0; jobsCache.length = 0;
jobsCache.push(...items); jobsCache.push(...items);
applyFiltersAndRender(); applyFiltersAndRender();
const loadTime = Math.round(performance.now() - startTime); const loadTime = Math.round(performance.now() - startTime);
console.log(`✓ Loaded ${items.length} jobs in ${loadTime}ms`); console.log(`✓ Loaded ${items.length} jobs in ${loadTime}ms (page 1 of ${data.totalPages})`);
// If there are more pages, load them in background
if (data.totalPages && data.totalPages > 1) {
loadRemainingPages(data.totalPages, 2);
} else {
allJobsFetched = true;
}
} catch(err) { } catch(err) {
alert('Failed to fetch jobs: '+err.message); if(err.name === 'AbortError') {
console.error(err); console.warn('Fetch timeout - server may be slow');
} else {
console.error('Fetch error:', err.message);
}
}
}
async function loadRemainingPages(totalPages, startPage) {
try {
for (let page = startPage; page <= totalPages; page++) {
const url = `/api/jobs-all?page=${page}&perPage=100`;
const res = await fetchNoCache(url);
if (!res.ok) break;
const data = await res.json();
const items = data.items || [];
jobsCache.push(...items);
applyFiltersAndRender();
console.log(`✓ Loaded page ${page} of ${totalPages} (${items.length} more jobs)`);
}
allJobsFetched = true;
} catch(err) {
console.warn('Error loading remaining pages:', err.message);
} }
} }