0e3750e755
- Type headers as Record<string, string> in apiRequest (client.js)
- Annotate SyncResult on result object, cast catch vars to any (sync.js)
- Widen sync.push param to Partial<Task>[] (endpoints.js)
- Fix parse() return type to reflect {task?: Task} shape (endpoints.js)
- Narrow add() param from Partial<Task> to Task (tasks.js)
- Cast parseAndCreate result to Task (tasks.js)
- Type tasksByProject grouped object as Record<string, Task[]> (tasks.js)
svelte-check now reports 0 errors and 0 warnings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
119 lines
2.7 KiB
JavaScript
119 lines
2.7 KiB
JavaScript
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';
|
|
|
|
/** @returns {string} */
|
|
function getClientId() {
|
|
let clientId = getItem(CLIENT_ID_KEY);
|
|
if (!clientId) {
|
|
clientId = generateUUID();
|
|
setItem(CLIENT_ID_KEY, clientId);
|
|
}
|
|
return clientId;
|
|
}
|
|
|
|
/** @returns {SyncState} */
|
|
function loadSyncState() {
|
|
const stored = getItem(SYNC_STATE_KEY);
|
|
return {
|
|
status: 'idle',
|
|
lastSync: stored?.lastSync || 0,
|
|
error: null,
|
|
queueSize: getQueue().length,
|
|
clientId: getClientId()
|
|
};
|
|
}
|
|
|
|
function createSyncStore() {
|
|
const { subscribe, set, update } = writable(loadSyncState());
|
|
|
|
return {
|
|
subscribe,
|
|
|
|
/** @returns {Promise<SyncResult>} */
|
|
async sync() {
|
|
update(state => ({ ...state, status: 'syncing', error: null }));
|
|
|
|
try {
|
|
const state = loadSyncState();
|
|
const queue = getQueue();
|
|
|
|
/** @type {SyncResult} */
|
|
const result = {
|
|
pulled: 0,
|
|
pushed: 0,
|
|
conflicts_resolved: 0,
|
|
queued_offline: 0,
|
|
errors: []
|
|
};
|
|
|
|
if (queue.length > 0) {
|
|
const tasks = queue.map(q => q.data);
|
|
try {
|
|
await syncAPI.push(tasks, state.clientId);
|
|
clearQueue();
|
|
result.pushed = queue.length;
|
|
} catch (/** @type {any} */ error) {
|
|
result.errors.push(`Failed to push queue: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const changes = await syncAPI.getChanges(state.lastSync, state.clientId);
|
|
result.pulled = changes.length;
|
|
// TODO: Apply changes to local state
|
|
} catch (/** @type {any} */ error) {
|
|
result.errors.push(`Failed to pull changes: ${error.message}`);
|
|
}
|
|
|
|
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 (/** @type {any} */ error) {
|
|
update(state => ({
|
|
...state,
|
|
status: 'error',
|
|
error: error.message
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
updateQueueSize() {
|
|
update(state => ({
|
|
...state,
|
|
queueSize: getQueue().length
|
|
}));
|
|
}
|
|
};
|
|
}
|
|
|
|
export const syncStore = createSyncStore();
|