CarboAny / src /lib /offlineQueue.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
4.59 kB
/**
* offlineQueue.ts β€” WS-2 Field Expansion
* μ˜€ν”„λΌμΈ 쀑 제좜 μš”μ²­μ„ IndexedDB에 νμž‰ν•˜κ³ ,
* 온라인 볡귀 μ‹œ Background Sync(λ˜λŠ” 폴링) 둜 μž¬μ „μ†‘.
*/
export interface QueueItem {
id?: number;
url: string;
method: string;
headers: Record<string, string>;
body: string;
enqueuedAt: number;
/** 원본 λ―Έμ…˜ ID β€” UI ν‘œμ‹œμš© */
missionId: string | null;
/** μ‚¬μš©μž ID */
userId: string;
}
interface SyncRegistration extends ServiceWorkerRegistration {
sync: {
register: (tag: string) => Promise<void>;
};
}
// ── DB μ΄ˆκΈ°ν™” ─────────────────────────────────────────────────────────────────
const DB_NAME = 'carboany-queue';
const DB_VERSION = 1;
const STORE = 'queue';
let _db: IDBDatabase | null = null;
function openDb(): Promise<IDBDatabase> {
if (_db) return Promise.resolve(_db);
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE)) {
db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
}
};
req.onsuccess = (e) => {
_db = (e.target as IDBOpenDBRequest).result;
resolve(_db!);
};
req.onerror = () => reject(req.error);
});
}
// ── Enqueue ───────────────────────────────────────────────────────────────────
export async function enqueueSubmission(item: Omit<QueueItem, 'id' | 'enqueuedAt'>): Promise<void> {
const db = await openDb();
const entry: Omit<QueueItem, 'id'> = { ...item, enqueuedAt: Date.now() };
await new Promise<void>((res, rej) => {
const tx = db.transaction(STORE, 'readwrite');
const req = tx.objectStore(STORE).add(entry);
req.onsuccess = () => res();
req.onerror = () => rej(req.error);
});
// Background Sync 등둝 (μ§€μ›ν•˜λŠ” λΈŒλΌμš°μ €)
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const reg = await navigator.serviceWorker.ready;
await (reg as SyncRegistration).sync.register('carboany-offline-queue').catch(() => {});
}
}
// ── Flush (온라인 볡귀 μ‹œ 호좜) ───────────────────────────────────────────────
export async function flushQueue(
onSuccess?: (item: QueueItem) => void,
onError?: (item: QueueItem, err: unknown) => void
): Promise<void> {
const db = await openDb();
const items = await getAllItems(db);
if (items.length === 0) return;
for (const item of items) {
try {
const res = await fetch(item.url, {
method: item.method,
headers: item.headers,
body: item.body,
});
if (res.ok) {
await deleteItem(db, item.id!);
onSuccess?.(item);
} else {
onError?.(item, new Error(`HTTP ${res.status}`));
}
} catch (err) {
onError?.(item, err);
}
}
}
// ── 큐 크기 쑰회 ──────────────────────────────────────────────────────────────
export async function getQueueSize(): Promise<number> {
const db = await openDb();
return new Promise((res, rej) => {
const tx = db.transaction(STORE, 'readonly');
const req = tx.objectStore(STORE).count();
req.onsuccess = () => res(req.result);
req.onerror = () => rej(req.error);
});
}
// ── λ‚΄λΆ€ μœ ν‹Έ ─────────────────────────────────────────────────────────────────
function getAllItems(db: IDBDatabase): Promise<QueueItem[]> {
return new Promise((res, rej) => {
const tx = db.transaction(STORE, 'readonly');
const req = tx.objectStore(STORE).getAll();
req.onsuccess = () => res(req.result as QueueItem[]);
req.onerror = () => rej(req.error);
});
}
function deleteItem(db: IDBDatabase, id: number): Promise<void> {
return new Promise((res, rej) => {
const tx = db.transaction(STORE, 'readwrite');
const req = tx.objectStore(STORE).delete(id);
req.onsuccess = () => res();
req.onerror = () => rej(req.error);
});
}