Some ignored filles was missing

This commit is contained in:
Janus C. H. Knudsen 2026-02-03 00:02:25 +01:00
parent 7db22245e2
commit fd5ab6bc0d
268 changed files with 31970 additions and 4 deletions

View file

@ -0,0 +1,97 @@
import { ICalendarEvent } from '../types/CalendarTypes';
/**
* Operation for the sync queue
*/
export interface IQueueOperation {
id: string;
type: 'create' | 'update' | 'delete';
eventId: string;
data: Partial<ICalendarEvent> | ICalendarEvent;
timestamp: number;
retryCount: number;
}
/**
* IndexedDB Service for Calendar App
* Handles local storage of events and sync queue
*/
export declare class IndexedDBService {
private static readonly DB_NAME;
private static readonly DB_VERSION;
private static readonly EVENTS_STORE;
private static readonly QUEUE_STORE;
private static readonly SYNC_STATE_STORE;
private db;
private initialized;
/**
* Initialize and open the database
*/
initialize(): Promise<void>;
/**
* Check if database is initialized
*/
isInitialized(): boolean;
/**
* Ensure database is initialized
*/
private ensureDB;
/**
* Get a single event by ID
*/
getEvent(id: string): Promise<ICalendarEvent | null>;
/**
* Get all events
*/
getAllEvents(): Promise<ICalendarEvent[]>;
/**
* Save an event (create or update)
*/
saveEvent(event: ICalendarEvent): Promise<void>;
/**
* Delete an event
*/
deleteEvent(id: string): Promise<void>;
/**
* Add operation to queue
*/
addToQueue(operation: Omit<IQueueOperation, 'id'>): Promise<void>;
/**
* Get all queue operations (sorted by timestamp)
*/
getQueue(): Promise<IQueueOperation[]>;
/**
* Remove operation from queue
*/
removeFromQueue(id: string): Promise<void>;
/**
* Clear entire queue
*/
clearQueue(): Promise<void>;
/**
* Save sync state value
*/
setSyncState(key: string, value: any): Promise<void>;
/**
* Get sync state value
*/
getSyncState(key: string): Promise<any | null>;
/**
* Serialize event for IndexedDB storage (convert Dates to ISO strings)
*/
private serializeEvent;
/**
* Deserialize event from IndexedDB (convert ISO strings to Dates)
*/
private deserializeEvent;
/**
* Close database connection
*/
close(): void;
/**
* Delete entire database (for testing/reset)
*/
static deleteDatabase(): Promise<void>;
/**
* Seed IndexedDB with mock data if empty
*/
seedIfEmpty(mockDataUrl?: string): Promise<void>;
}

View file

@ -0,0 +1,340 @@
/**
* IndexedDB Service for Calendar App
* Handles local storage of events and sync queue
*/
export class IndexedDBService {
constructor() {
this.db = null;
this.initialized = false;
}
/**
* Initialize and open the database
*/
async initialize() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(IndexedDBService.DB_NAME, IndexedDBService.DB_VERSION);
request.onerror = () => {
reject(new Error(`Failed to open IndexedDB: ${request.error}`));
};
request.onsuccess = () => {
this.db = request.result;
this.initialized = true;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create events store
if (!db.objectStoreNames.contains(IndexedDBService.EVENTS_STORE)) {
const eventsStore = db.createObjectStore(IndexedDBService.EVENTS_STORE, { keyPath: 'id' });
eventsStore.createIndex('start', 'start', { unique: false });
eventsStore.createIndex('end', 'end', { unique: false });
eventsStore.createIndex('syncStatus', 'syncStatus', { unique: false });
}
// Create operation queue store
if (!db.objectStoreNames.contains(IndexedDBService.QUEUE_STORE)) {
const queueStore = db.createObjectStore(IndexedDBService.QUEUE_STORE, { keyPath: 'id' });
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
}
// Create sync state store
if (!db.objectStoreNames.contains(IndexedDBService.SYNC_STATE_STORE)) {
db.createObjectStore(IndexedDBService.SYNC_STATE_STORE, { keyPath: 'key' });
}
};
});
}
/**
* Check if database is initialized
*/
isInitialized() {
return this.initialized;
}
/**
* Ensure database is initialized
*/
ensureDB() {
if (!this.db) {
throw new Error('IndexedDB not initialized. Call initialize() first.');
}
return this.db;
}
// ========================================
// Event CRUD Operations
// ========================================
/**
* Get a single event by ID
*/
async getEvent(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.get(id);
request.onsuccess = () => {
const event = request.result;
resolve(event ? this.deserializeEvent(event) : null);
};
request.onerror = () => {
reject(new Error(`Failed to get event ${id}: ${request.error}`));
};
});
}
/**
* Get all events
*/
async getAllEvents() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.getAll();
request.onsuccess = () => {
const events = request.result;
resolve(events.map(e => this.deserializeEvent(e)));
};
request.onerror = () => {
reject(new Error(`Failed to get all events: ${request.error}`));
};
});
}
/**
* Save an event (create or update)
*/
async saveEvent(event) {
const db = this.ensureDB();
const serialized = this.serializeEvent(event);
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.put(serialized);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to save event ${event.id}: ${request.error}`));
};
});
}
/**
* Delete an event
*/
async deleteEvent(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete event ${id}: ${request.error}`));
};
});
}
// ========================================
// Queue Operations
// ========================================
/**
* Add operation to queue
*/
async addToQueue(operation) {
const db = this.ensureDB();
const queueItem = {
...operation,
id: `${operation.type}-${operation.eventId}-${Date.now()}`
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.put(queueItem);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to add to queue: ${request.error}`));
};
});
}
/**
* Get all queue operations (sorted by timestamp)
*/
async getQueue() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const index = store.index('timestamp');
const request = index.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(new Error(`Failed to get queue: ${request.error}`));
};
});
}
/**
* Remove operation from queue
*/
async removeFromQueue(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to remove from queue: ${request.error}`));
};
});
}
/**
* Clear entire queue
*/
async clearQueue() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.clear();
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to clear queue: ${request.error}`));
};
});
}
// ========================================
// Sync State Operations
// ========================================
/**
* Save sync state value
*/
async setSyncState(key, value) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE);
const request = store.put({ key, value });
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to set sync state ${key}: ${request.error}`));
};
});
}
/**
* Get sync state value
*/
async getSyncState(key) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE);
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
resolve(result ? result.value : null);
};
request.onerror = () => {
reject(new Error(`Failed to get sync state ${key}: ${request.error}`));
};
});
}
// ========================================
// Serialization Helpers
// ========================================
/**
* Serialize event for IndexedDB storage (convert Dates to ISO strings)
*/
serializeEvent(event) {
return {
...event,
start: event.start instanceof Date ? event.start.toISOString() : event.start,
end: event.end instanceof Date ? event.end.toISOString() : event.end
};
}
/**
* Deserialize event from IndexedDB (convert ISO strings to Dates)
*/
deserializeEvent(event) {
return {
...event,
start: typeof event.start === 'string' ? new Date(event.start) : event.start,
end: typeof event.end === 'string' ? new Date(event.end) : event.end
};
}
/**
* Close database connection
*/
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
/**
* Delete entire database (for testing/reset)
*/
static async deleteDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(IndexedDBService.DB_NAME);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete database: ${request.error}`));
};
});
}
/**
* Seed IndexedDB with mock data if empty
*/
async seedIfEmpty(mockDataUrl = 'data/mock-events.json') {
try {
const existingEvents = await this.getAllEvents();
if (existingEvents.length > 0) {
console.log(`IndexedDB already has ${existingEvents.length} events - skipping seed`);
return;
}
console.log('IndexedDB is empty - seeding with mock data');
// Check if online to fetch mock data
if (!navigator.onLine) {
console.warn('Offline and IndexedDB empty - starting with no events');
return;
}
// Fetch mock events
const response = await fetch(mockDataUrl);
if (!response.ok) {
throw new Error(`Failed to fetch mock events: ${response.statusText}`);
}
const mockEvents = await response.json();
// Convert and save to IndexedDB
for (const event of mockEvents) {
const calendarEvent = {
...event,
start: new Date(event.start),
end: new Date(event.end),
allDay: event.allDay || false,
syncStatus: 'synced'
};
await this.saveEvent(calendarEvent);
}
console.log(`Seeded IndexedDB with ${mockEvents.length} mock events`);
}
catch (error) {
console.error('Failed to seed IndexedDB:', error);
// Don't throw - allow app to start with empty calendar
}
}
}
IndexedDBService.DB_NAME = 'CalendarDB';
IndexedDBService.DB_VERSION = 1;
IndexedDBService.EVENTS_STORE = 'events';
IndexedDBService.QUEUE_STORE = 'operationQueue';
IndexedDBService.SYNC_STATE_STORE = 'syncState';
//# sourceMappingURL=IndexedDBService.js.map

File diff suppressed because one or more lines are too long

55
wwwroot/js/storage/OperationQueue.d.ts vendored Normal file
View file

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

View file

@ -0,0 +1,96 @@
/**
* Operation Queue Manager
* Handles FIFO queue of pending sync operations
*/
export class OperationQueue {
constructor(indexedDB) {
this.indexedDB = indexedDB;
}
/**
* Add operation to the end of the queue
*/
async enqueue(operation) {
await this.indexedDB.addToQueue(operation);
}
/**
* Get the first operation from the queue (without removing it)
* Returns null if queue is empty
*/
async peek() {
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() {
return await this.indexedDB.getQueue();
}
/**
* Remove a specific operation from the queue
*/
async remove(operationId) {
await this.indexedDB.removeFromQueue(operationId);
}
/**
* Remove the first operation from the queue and return it
* Returns null if queue is empty
*/
async dequeue() {
const operation = await this.peek();
if (operation) {
await this.remove(operation.id);
}
return operation;
}
/**
* Clear all operations from the queue
*/
async clear() {
await this.indexedDB.clearQueue();
}
/**
* Get the number of operations in the queue
*/
async size() {
const queue = await this.getAll();
return queue.length;
}
/**
* Check if queue is empty
*/
async isEmpty() {
const size = await this.size();
return size === 0;
}
/**
* Get operations for a specific event ID
*/
async getOperationsForEvent(eventId) {
const queue = await this.getAll();
return queue.filter(op => op.eventId === eventId);
}
/**
* Remove all operations for a specific event ID
*/
async removeOperationsForEvent(eventId) {
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) {
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);
}
}
}
//# sourceMappingURL=OperationQueue.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"OperationQueue.js","sourceRoot":"","sources":["../../../src/storage/OperationQueue.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,OAAO,cAAc;IAGzB,YAAY,SAA2B;QACrC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,SAAsC;QAClD,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI;QACR,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM;QACV,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;IACzC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,WAAmB;QAC9B,MAAM,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;IACpC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI;QACR,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC,MAAM,CAAC;IACtB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,OAAO,IAAI,KAAK,CAAC,CAAC;IACpB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,qBAAqB,CAAC,OAAe;QACzC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;IACpD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,wBAAwB,CAAC,OAAe;QAC5C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;QAC7D,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,mBAAmB,CAAC,WAAmB;QAC3C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;QAE1D,IAAI,SAAS,EAAE,CAAC;YACd,SAAS,CAAC,UAAU,EAAE,CAAC;YACvB,2CAA2C;YAC3C,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC/B,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;CACF"}