From 1bc9dc2dd56aec9100633a148c88d648118c200d Mon Sep 17 00:00:00 2001 From: RKWojs Date: Thu, 4 Dec 2025 11:56:07 +0100 Subject: [PATCH] feat: refactor Radicale contact sync logic to include retry mechanism and improve error handling --- src/lib/radicale-sync.js | 92 +++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/src/lib/radicale-sync.js b/src/lib/radicale-sync.js index b280a76..d427964 100644 --- a/src/lib/radicale-sync.js +++ b/src/lib/radicale-sync.js @@ -138,6 +138,58 @@ export function isRadicaleEnabled() { return getRadicaleConfig() !== null; } +// Sync contact to Radicale (internal function with retry logic) +async function syncContactToRadicaleInternal(contact, forceUpdate = false) { + const config = getRadicaleConfig(); + + // Skip if not configured + if (!config) { + return { success: false, reason: 'not_configured' }; + } + + try { + const vcard = generateVCard(contact); + const auth = Buffer.from(`${config.username}:${config.password}`).toString('base64'); + + // Ensure URL ends with / + const baseUrl = config.url.endsWith('/') ? config.url : config.url + '/'; + + // Construct the URL for this specific contact + const vcardUrl = `${baseUrl}${config.username}/contacts/contact-${contact.contact_id}.vcf`; + + const headers = { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'text/vcard; charset=utf-8' + }; + + // If not forcing update, only create if doesn't exist + if (!forceUpdate) { + headers['If-None-Match'] = '*'; + } + + const response = await fetch(vcardUrl, { + method: 'PUT', + headers: headers, + body: vcard + }); + + // Handle conflict - try again with force update + if (response.status === 412 || response.status === 409) { + // Conflict - contact already exists, update it instead + return await syncContactToRadicaleInternal(contact, true); + } + + if (response.ok || response.status === 201 || response.status === 204) { + return { success: true, status: response.status, updated: forceUpdate }; + } else { + const text = await response.text(); + return { success: false, status: response.status, error: text }; + } + } catch (error) { + return { success: false, error: error.message }; + } +} + // Sync contact to Radicale export async function syncContactToRadicale(contact) { const config = getRadicaleConfig(); @@ -154,37 +206,16 @@ export async function syncContactToRadicale(contact) { return { success: true, reason: 'inactive_skipped' }; } - try { - const vcard = generateVCard(contact); - const auth = Buffer.from(`${config.username}:${config.password}`).toString('base64'); - - // Ensure URL ends with / - const baseUrl = config.url.endsWith('/') ? config.url : config.url + '/'; - - // Construct the URL for this specific contact - const vcardUrl = `${baseUrl}${config.username}/contacts/contact-${contact.contact_id}.vcf`; - - const response = await fetch(vcardUrl, { - method: 'PUT', - headers: { - 'Authorization': `Basic ${auth}`, - 'Content-Type': 'text/vcard; charset=utf-8' - }, - body: vcard - }); - - if (response.ok || response.status === 201 || response.status === 204) { - console.log(`✅ Synced contact ${contact.contact_id} to Radicale`); - return { success: true, status: response.status }; - } else { - const text = await response.text(); - console.error(`❌ Failed to sync contact ${contact.contact_id} to Radicale: ${response.status} - ${text}`); - return { success: false, status: response.status, error: text }; - } - } catch (error) { - console.error(`❌ Error syncing contact ${contact.contact_id} to Radicale:`, error); - return { success: false, error: error.message }; + const result = await syncContactToRadicaleInternal(contact); + + if (result.success) { + const action = result.updated ? 'updated' : 'created'; + console.log(`✅ Synced contact ${contact.contact_id} to Radicale (${action})`); + } else if (result.reason !== 'not_configured') { + console.error(`❌ Failed to sync contact ${contact.contact_id} to Radicale: ${result.status} - ${result.error || result.reason}`); } + + return result; } // Delete contact from Radicale @@ -229,6 +260,7 @@ export async function deleteContactFromRadicale(contactId) { // Sync contact asynchronously (fire and forget) export function syncContactAsync(contact) { + console.log(contact, isRadicaleEnabled()); if (!isRadicaleEnabled()) { return; }