Implements offline-first calendar sync infrastructure

Adds IndexedDB and operation queue for robust offline synchronization
Introduces SyncManager to handle background data synchronization
Supports local event operations with automatic remote sync queuing

Enhances application reliability and user experience in low/no connectivity scenarios
This commit is contained in:
Janus C. H. Knudsen 2025-11-05 00:37:57 +01:00
parent 9c765b35ab
commit e7011526e3
20 changed files with 3822 additions and 57 deletions

View file

@ -0,0 +1,111 @@
import { IndexedDBService, IQueueOperation } from './IndexedDBService';
/**
* Operation Queue Manager
* Handles FIFO queue of pending sync operations
*/
export class OperationQueue {
private indexedDB: IndexedDBService;
constructor(indexedDB: IndexedDBService) {
this.indexedDB = indexedDB;
}
/**
* Add operation to the end of the queue
*/
async enqueue(operation: Omit<IQueueOperation, 'id'>): Promise<void> {
await this.indexedDB.addToQueue(operation);
}
/**
* Get the first operation from the queue (without removing it)
* Returns null if queue is empty
*/
async peek(): Promise<IQueueOperation | null> {
const queue = await this.indexedDB.getQueue();
return queue.length > 0 ? queue[0] : null;
}
/**
* Get all operations in the queue (sorted by timestamp FIFO)
*/
async getAll(): Promise<IQueueOperation[]> {
return await this.indexedDB.getQueue();
}
/**
* Remove a specific operation from the queue
*/
async remove(operationId: string): Promise<void> {
await this.indexedDB.removeFromQueue(operationId);
}
/**
* Remove the first operation from the queue and return it
* Returns null if queue is empty
*/
async dequeue(): Promise<IQueueOperation | null> {
const operation = await this.peek();
if (operation) {
await this.remove(operation.id);
}
return operation;
}
/**
* Clear all operations from the queue
*/
async clear(): Promise<void> {
await this.indexedDB.clearQueue();
}
/**
* Get the number of operations in the queue
*/
async size(): Promise<number> {
const queue = await this.getAll();
return queue.length;
}
/**
* Check if queue is empty
*/
async isEmpty(): Promise<boolean> {
const size = await this.size();
return size === 0;
}
/**
* Get operations for a specific event ID
*/
async getOperationsForEvent(eventId: string): Promise<IQueueOperation[]> {
const queue = await this.getAll();
return queue.filter(op => op.eventId === eventId);
}
/**
* Remove all operations for a specific event ID
*/
async removeOperationsForEvent(eventId: string): Promise<void> {
const operations = await this.getOperationsForEvent(eventId);
for (const op of operations) {
await this.remove(op.id);
}
}
/**
* Update retry count for an operation
*/
async incrementRetryCount(operationId: string): Promise<void> {
const queue = await this.getAll();
const operation = queue.find(op => op.id === operationId);
if (operation) {
operation.retryCount++;
// Re-add to queue with updated retry count
await this.remove(operationId);
await this.enqueue(operation);
}
}
}