113 lines
3.6 KiB
JavaScript
113 lines
3.6 KiB
JavaScript
function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus, createOperationId }) {
|
|
const prefix = 'stackchain.today-sync.v1.';
|
|
const migrationPrefix = 'stackchain.today-sync-migrated.v1.';
|
|
let flushing = null;
|
|
|
|
function key() {
|
|
const login = String(getLogin?.() || '').trim().toLowerCase();
|
|
return login ? prefix + encodeURIComponent(login) : '';
|
|
}
|
|
|
|
function pending() {
|
|
const storageKey = key();
|
|
if (!storageKey || !storage) return [];
|
|
try {
|
|
const value = JSON.parse(storage.getItem(storageKey) || '[]');
|
|
return Array.isArray(value) ? value.filter(operation =>
|
|
operation && typeof operation.operation_id === 'string' &&
|
|
['add', 'remove', 'move'].includes(operation.action) &&
|
|
typeof operation.item_id === 'string'
|
|
) : [];
|
|
} catch (_error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function save(operations) {
|
|
const storageKey = key();
|
|
if (!storageKey || !storage) return false;
|
|
try {
|
|
if (operations.length) storage.setItem(storageKey, JSON.stringify(operations));
|
|
else storage.removeItem(storageKey);
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function operationId() {
|
|
if (createOperationId) return createOperationId();
|
|
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
|
|
}
|
|
|
|
function enqueue(action, itemId, direction = null) {
|
|
const operations = pending();
|
|
if (action === 'remove' && operations.some(operation =>
|
|
operation.action === 'remove' && operation.item_id === itemId
|
|
)) {
|
|
onStatus?.('pending');
|
|
return true;
|
|
}
|
|
operations.push({ operation_id: operationId(), action, item_id: itemId, direction });
|
|
const saved = save(operations);
|
|
onStatus?.(saved ? 'pending' : 'error');
|
|
return saved;
|
|
}
|
|
|
|
function migrate(ids) {
|
|
const storageKey = key();
|
|
if (!storageKey || !storage) return false;
|
|
const marker = migrationPrefix + storageKey.slice(prefix.length);
|
|
try {
|
|
if (storage.getItem(marker)) return false;
|
|
for (const id of ids || []) enqueue('add', id);
|
|
storage.setItem(marker, '1');
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
if (!key()) return false;
|
|
try {
|
|
let plan = await fetchJson('api/v1/today');
|
|
const operations = pending();
|
|
for (const operation of operations) {
|
|
try {
|
|
plan = await fetchJson('api/v1/today', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(operation),
|
|
});
|
|
} catch (error) {
|
|
if (error?.status !== 409) throw error;
|
|
const rejected = pending();
|
|
save(rejected.filter(candidate => candidate.operation_id !== operation.operation_id));
|
|
onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []);
|
|
onStatus?.('full');
|
|
return false;
|
|
}
|
|
const remaining = pending();
|
|
if (remaining[0]?.operation_id === operation.operation_id) save(remaining.slice(1));
|
|
}
|
|
onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []);
|
|
onStatus?.(pending().length ? 'pending' : 'saved');
|
|
return true;
|
|
} catch (_error) {
|
|
onStatus?.(pending().length ? 'pending' : 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function flush() {
|
|
if (!flushing) flushing = run().finally(() => { flushing = null; });
|
|
return flushing;
|
|
}
|
|
|
|
return { enqueue, migrate, flush, pending };
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createTodaySync;
|