generated from coulomb/repo-seed
Add PDF list view with upload status tracking
- Show all pending PDFs in a scrollable list instead of single PDF - Track upload status (pending/uploading/uploaded/failed) per PDF - Store queue in chrome.storage.local for persistence - Prevent duplicate uploads by checking URL against uploaded PDFs - Add Dismiss button to remove PDFs from queue - Show badge with count of pending PDFs - Auto-cleanup old entries (uploaded >7 days, failed >24h) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
226
src/utils/pdf-queue.ts
Normal file
226
src/utils/pdf-queue.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* PDF Queue storage utilities
|
||||
*
|
||||
* Manages a persistent list of detected PDFs with upload status tracking.
|
||||
* Uses chrome.storage.local for persistence across service worker restarts.
|
||||
*/
|
||||
|
||||
import { DetectedPDF } from './pdf-detector';
|
||||
|
||||
const STORAGE_KEY = 'pdfQueue';
|
||||
const MAX_ENTRIES = 50;
|
||||
const UPLOADED_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const FAILED_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export type PDFStatus = 'pending' | 'uploading' | 'uploaded' | 'failed';
|
||||
|
||||
export interface PDFQueueEntry extends DetectedPDF {
|
||||
status: PDFStatus;
|
||||
uploadedAt?: number;
|
||||
binectDocumentId?: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
interface PDFQueueState {
|
||||
entries: PDFQueueEntry[];
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load queue from storage
|
||||
*/
|
||||
export async function loadQueue(): Promise<PDFQueueState> {
|
||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
||||
if (result[STORAGE_KEY]) {
|
||||
return result[STORAGE_KEY] as PDFQueueState;
|
||||
}
|
||||
return { entries: [], lastUpdated: Date.now() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save queue to storage
|
||||
*/
|
||||
export async function saveQueue(state: PDFQueueState): Promise<void> {
|
||||
state.lastUpdated = Date.now();
|
||||
await chrome.storage.local.set({ [STORAGE_KEY]: state });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a PDF to the queue
|
||||
*
|
||||
* Returns the created entry, or null if the PDF was already uploaded.
|
||||
* If the PDF already exists as pending/failed, returns the existing entry.
|
||||
*/
|
||||
export async function addPDF(pdf: DetectedPDF): Promise<PDFQueueEntry | null> {
|
||||
const state = await loadQueue();
|
||||
|
||||
// Check for duplicate by URL
|
||||
const existing = state.entries.find(e => e.url === pdf.url);
|
||||
if (existing) {
|
||||
if (existing.status === 'uploaded') {
|
||||
console.log('[PDF Queue] PDF already uploaded, skipping:', pdf.filename);
|
||||
return null;
|
||||
}
|
||||
console.log('[PDF Queue] PDF already in queue:', pdf.filename);
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
const entry: PDFQueueEntry = {
|
||||
...pdf,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
// Add to beginning (most recent first)
|
||||
state.entries.unshift(entry);
|
||||
|
||||
// Enforce max entries
|
||||
await enforceMaxEntries(state);
|
||||
|
||||
await saveQueue(state);
|
||||
console.log('[PDF Queue] Added PDF:', pdf.filename);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of a PDF in the queue
|
||||
*/
|
||||
export async function updatePDFStatus(
|
||||
id: string,
|
||||
status: PDFStatus,
|
||||
meta?: { binectDocumentId?: number; errorMessage?: string }
|
||||
): Promise<void> {
|
||||
const state = await loadQueue();
|
||||
const entry = state.entries.find(e => e.id === id);
|
||||
|
||||
if (!entry) {
|
||||
console.warn('[PDF Queue] PDF not found for status update:', id);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.status = status;
|
||||
|
||||
if (status === 'uploaded') {
|
||||
entry.uploadedAt = Date.now();
|
||||
if (meta?.binectDocumentId) {
|
||||
entry.binectDocumentId = meta.binectDocumentId;
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'failed' && meta?.errorMessage) {
|
||||
entry.errorMessage = meta.errorMessage;
|
||||
}
|
||||
|
||||
await saveQueue(state);
|
||||
console.log('[PDF Queue] Updated status:', id, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a PDF from the queue
|
||||
*/
|
||||
export async function removePDF(id: string): Promise<void> {
|
||||
const state = await loadQueue();
|
||||
const index = state.entries.findIndex(e => e.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
console.warn('[PDF Queue] PDF not found for removal:', id);
|
||||
return;
|
||||
}
|
||||
|
||||
state.entries.splice(index, 1);
|
||||
await saveQueue(state);
|
||||
console.log('[PDF Queue] Removed PDF:', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending and failed PDFs (for display in popup)
|
||||
*/
|
||||
export async function getPendingPDFs(): Promise<PDFQueueEntry[]> {
|
||||
const state = await loadQueue();
|
||||
return state.entries.filter(e => e.status === 'pending' || e.status === 'failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of pending PDFs (for badge)
|
||||
*/
|
||||
export async function getPendingCount(): Promise<number> {
|
||||
const pending = await getPendingPDFs();
|
||||
return pending.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old entries to prevent unbounded growth
|
||||
*/
|
||||
export async function cleanupOldEntries(): Promise<void> {
|
||||
const state = await loadQueue();
|
||||
const now = Date.now();
|
||||
const initialCount = state.entries.length;
|
||||
|
||||
state.entries = state.entries.filter(entry => {
|
||||
// Always keep pending entries
|
||||
if (entry.status === 'pending' || entry.status === 'uploading') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove uploaded entries older than 7 days
|
||||
if (entry.status === 'uploaded' && entry.uploadedAt) {
|
||||
const age = now - entry.uploadedAt;
|
||||
if (age > UPLOADED_MAX_AGE_MS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove failed entries older than 24 hours
|
||||
if (entry.status === 'failed') {
|
||||
const age = now - entry.timestamp;
|
||||
if (age > FAILED_MAX_AGE_MS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (state.entries.length < initialCount) {
|
||||
await saveQueue(state);
|
||||
console.log('[PDF Queue] Cleaned up', initialCount - state.entries.length, 'old entries');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce maximum entries by removing oldest uploaded/failed entries
|
||||
*/
|
||||
async function enforceMaxEntries(state: PDFQueueState): Promise<void> {
|
||||
while (state.entries.length > MAX_ENTRIES) {
|
||||
// Find oldest uploaded entry
|
||||
let removeIndex = -1;
|
||||
let oldestTime = Infinity;
|
||||
|
||||
for (let i = state.entries.length - 1; i >= 0; i--) {
|
||||
const entry = state.entries[i];
|
||||
if (entry.status === 'uploaded' && entry.uploadedAt && entry.uploadedAt < oldestTime) {
|
||||
oldestTime = entry.uploadedAt;
|
||||
removeIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If no uploaded entries, find oldest failed
|
||||
if (removeIndex === -1) {
|
||||
for (let i = state.entries.length - 1; i >= 0; i--) {
|
||||
const entry = state.entries[i];
|
||||
if (entry.status === 'failed' && entry.timestamp < oldestTime) {
|
||||
oldestTime = entry.timestamp;
|
||||
removeIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still nothing, we can't remove any more (all pending)
|
||||
if (removeIndex === -1) {
|
||||
console.warn('[PDF Queue] Max entries reached, but all are pending');
|
||||
break;
|
||||
}
|
||||
|
||||
state.entries.splice(removeIndex, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user