Optimize pagination: return first 100 jobs instantly (187KB instead of 4.3MB), load remaining pages in background
This commit is contained in:
+82
-43
@@ -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) {
|
|
||||||
return c.json({
|
|
||||||
page: 1,
|
|
||||||
perPage: partialJobs.length,
|
|
||||||
totalItems: partialJobs.length,
|
|
||||||
totalPages: 1,
|
|
||||||
items: partialJobs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ error: 'Fetching jobs, try again' }, 503);
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
|
logLine('logs/error.log', `jobs-all endpoint error: ${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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user