136 lines
3.6 KiB
JavaScript
136 lines
3.6 KiB
JavaScript
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||
|
|
import {
|
||
|
|
addScannedCardToOwned,
|
||
|
|
addScannedCardToCollection,
|
||
|
|
addScannedCardToDeck,
|
||
|
|
} from './scanner-route-api.js';
|
||
|
|
|
||
|
|
const OFFLINE_QUEUE_KEY = 'deckhearth:offline-scan-queue';
|
||
|
|
const MAX_RETRIES = 3;
|
||
|
|
|
||
|
|
function loadQueue() {
|
||
|
|
if (typeof window === 'undefined') return [];
|
||
|
|
try {
|
||
|
|
return JSON.parse(localStorage.getItem(OFFLINE_QUEUE_KEY)) || [];
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function persistQueue(queue) {
|
||
|
|
if (typeof window === 'undefined') return;
|
||
|
|
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function executeAction(action, payload) {
|
||
|
|
switch (action) {
|
||
|
|
case 'addToOwned':
|
||
|
|
return addScannedCardToOwned(payload);
|
||
|
|
case 'addToCollection':
|
||
|
|
return addScannedCardToCollection(payload, payload.collectionId);
|
||
|
|
case 'addToDeck':
|
||
|
|
return addScannedCardToDeck(payload, payload.deckId);
|
||
|
|
default:
|
||
|
|
throw new Error(`Unknown offline action: ${action}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useScannerOffline() {
|
||
|
|
const [isOnline, setIsOnline] = useState(
|
||
|
|
() => typeof navigator !== 'undefined' ? navigator.onLine : true
|
||
|
|
);
|
||
|
|
const [queuedActions, setQueuedActions] = useState(loadQueue);
|
||
|
|
const processingRef = useRef(false);
|
||
|
|
|
||
|
|
const updateQueue = useCallback((updater) => {
|
||
|
|
setQueuedActions((prev) => {
|
||
|
|
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||
|
|
persistQueue(next);
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const processQueue = useCallback(async () => {
|
||
|
|
if (processingRef.current) return;
|
||
|
|
processingRef.current = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const current = loadQueue();
|
||
|
|
const pending = current.filter((item) => item.status === 'pending');
|
||
|
|
|
||
|
|
for (const item of pending) {
|
||
|
|
try {
|
||
|
|
await executeAction(item.action, item.payload);
|
||
|
|
updateQueue((prev) => prev.filter((q) => q.id !== item.id));
|
||
|
|
} catch {
|
||
|
|
updateQueue((prev) =>
|
||
|
|
prev.map((q) => {
|
||
|
|
if (q.id !== item.id) return q;
|
||
|
|
const retries = q.retries + 1;
|
||
|
|
return {
|
||
|
|
...q,
|
||
|
|
retries,
|
||
|
|
status: retries >= MAX_RETRIES ? 'failed' : 'pending',
|
||
|
|
};
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
processingRef.current = false;
|
||
|
|
}
|
||
|
|
}, [updateQueue]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const goOnline = () => {
|
||
|
|
setIsOnline(true);
|
||
|
|
processQueue();
|
||
|
|
};
|
||
|
|
const goOffline = () => setIsOnline(false);
|
||
|
|
|
||
|
|
window.addEventListener('online', goOnline);
|
||
|
|
window.addEventListener('offline', goOffline);
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener('online', goOnline);
|
||
|
|
window.removeEventListener('offline', goOffline);
|
||
|
|
};
|
||
|
|
}, [processQueue]);
|
||
|
|
|
||
|
|
// Process any items left over from a previous session on mount
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOnline && loadQueue().some((q) => q.status === 'pending')) {
|
||
|
|
processQueue();
|
||
|
|
}
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const enqueue = useCallback(
|
||
|
|
async (action, payload) => {
|
||
|
|
const item = {
|
||
|
|
id: Date.now() + Math.random(),
|
||
|
|
action,
|
||
|
|
payload,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
retries: 0,
|
||
|
|
status: 'pending',
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isOnline) {
|
||
|
|
try {
|
||
|
|
await executeAction(action, payload);
|
||
|
|
return;
|
||
|
|
} catch {
|
||
|
|
// Network error while supposedly online — fall through to queue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
updateQueue((prev) => [...prev, item]);
|
||
|
|
},
|
||
|
|
[isOnline, updateQueue]
|
||
|
|
);
|
||
|
|
|
||
|
|
const pendingCount = queuedActions.filter((q) => q.status === 'pending').length;
|
||
|
|
|
||
|
|
return { isOnline, queuedActions, enqueue, processQueue, pendingCount };
|
||
|
|
}
|