File size: 4,587 Bytes
5c98911 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | /**
* 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);
});
}
|