feat(frontend): add API client and Svelte stores
- Create API client with auto-retry and token refresh support - Add comprehensive API endpoints for tasks, tags, projects, sync, and auth - Implement authStore for authentication state management - Implement tasksStore with optimistic updates and offline queue - Add derived stores for filtered task views (pending, completed, by project) - Implement syncStore for managing sync state and queue - Add client ID generation and persistence for sync tracking
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { sync as syncAPI } from '$lib/api/endpoints.js';
|
||||
import { getQueue, clearQueue } from '$lib/utils/sync-queue.js';
|
||||
import { getItem, setItem } from '$lib/utils/storage.js';
|
||||
import { generateUUID } from '$lib/utils/uuid.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('$lib/api/types.js').SyncResult} SyncResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SyncState
|
||||
* @property {'idle'|'syncing'|'error'} status
|
||||
* @property {number} lastSync - Unix timestamp
|
||||
* @property {string|null} error
|
||||
* @property {number} queueSize
|
||||
* @property {string} clientId
|
||||
*/
|
||||
|
||||
const SYNC_STATE_KEY = 'opal_sync_state';
|
||||
const CLIENT_ID_KEY = 'opal_client_id';
|
||||
|
||||
/**
|
||||
* Get or create client ID
|
||||
* @returns {string}
|
||||
*/
|
||||
function getClientId() {
|
||||
let clientId = getItem(CLIENT_ID_KEY);
|
||||
if (!clientId) {
|
||||
clientId = generateUUID();
|
||||
setItem(CLIENT_ID_KEY, clientId);
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load sync state
|
||||
* @returns {SyncState}
|
||||
*/
|
||||
function loadSyncState() {
|
||||
const stored = getItem(SYNC_STATE_KEY);
|
||||
return {
|
||||
status: 'idle',
|
||||
lastSync: stored?.lastSync || 0,
|
||||
error: null,
|
||||
queueSize: getQueue().length,
|
||||
clientId: getClientId()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sync store
|
||||
*/
|
||||
function createSyncStore() {
|
||||
const { subscribe, set, update } = writable(loadSyncState());
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
/**
|
||||
* Perform sync
|
||||
* @returns {Promise<SyncResult>}
|
||||
*/
|
||||
async sync() {
|
||||
update(state => ({ ...state, status: 'syncing', error: null }));
|
||||
|
||||
try {
|
||||
const state = loadSyncState();
|
||||
const queue = getQueue();
|
||||
|
||||
let result = {
|
||||
pulled: 0,
|
||||
pushed: 0,
|
||||
conflicts_resolved: 0,
|
||||
queued_offline: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
// Push queued changes
|
||||
if (queue.length > 0) {
|
||||
const tasks = queue.map(q => q.data);
|
||||
try {
|
||||
await syncAPI.push(tasks, state.clientId);
|
||||
clearQueue();
|
||||
result.pushed = queue.length;
|
||||
} catch (error) {
|
||||
result.errors.push(`Failed to push queue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Pull changes from server
|
||||
try {
|
||||
const changes = await syncAPI.getChanges(state.lastSync, state.clientId);
|
||||
result.pulled = changes.length;
|
||||
// TODO: Apply changes to local state
|
||||
} catch (error) {
|
||||
result.errors.push(`Failed to pull changes: ${error.message}`);
|
||||
}
|
||||
|
||||
// Update sync state
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
setItem(SYNC_STATE_KEY, { lastSync: now });
|
||||
|
||||
update(state => ({
|
||||
...state,
|
||||
status: 'idle',
|
||||
lastSync: now,
|
||||
queueSize: 0,
|
||||
error: null
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
update(state => ({
|
||||
...state,
|
||||
status: 'error',
|
||||
error: error.message
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update queue size
|
||||
*/
|
||||
updateQueueSize() {
|
||||
update(state => ({
|
||||
...state,
|
||||
queueSize: getQueue().length
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const syncStore = createSyncStore();
|
||||
Reference in New Issue
Block a user