|
11 | 11 | import ImportOPMLModal from '$lib/components/ImportOPMLModal.svelte'; |
12 | 12 | import PageHeader from '$lib/components/common/PageHeader.svelte'; |
13 | 13 | import { downloadOPML } from '$lib/utils/opml-exporter'; |
14 | | - import { api } from '$lib/services/api'; |
| 14 | + import { api, RateLimitError } from '$lib/services/api'; |
15 | 15 |
|
16 | 16 | const fontOptions: { value: ArticleFont; label: string }[] = [ |
17 | 17 | { value: 'sans-serif', label: 'Sans Serif' }, |
|
32 | 32 | // PDS Sync state |
33 | 33 | let pdsSyncEnabled = $state(false); |
34 | 34 | let lastSyncSubscriptions = $state<number | null>(null); |
35 | | - let lastSyncReadPositions = $state<number | null>(null); |
36 | 35 | let isSyncLoading = $state(false); |
37 | 36 | let isSyncing = $state(false); |
38 | 37 | let syncError = $state<string | null>(null); |
|
58 | 57 | const settings = await api.getSettings(); |
59 | 58 | pdsSyncEnabled = settings.pdsSyncEnabled; |
60 | 59 | lastSyncSubscriptions = settings.lastPdsSyncSubscriptions; |
61 | | - lastSyncReadPositions = settings.lastPdsSyncReadPositions; |
62 | 60 | } catch (error) { |
63 | 61 | console.error('Failed to load sync settings:', error); |
64 | 62 | } finally { |
|
89 | 87 | } |
90 | 88 | } |
91 | 89 |
|
| 90 | + function sleep(ms: number): Promise<void> { |
| 91 | + return new Promise((resolve) => setTimeout(resolve, ms)); |
| 92 | + } |
| 93 | +
|
92 | 94 | async function handleSync() { |
93 | 95 | if (isSyncing) return; |
94 | 96 |
|
95 | 97 | isSyncing = true; |
96 | 98 | syncError = null; |
97 | 99 | syncSuccess = null; |
98 | 100 |
|
| 101 | + // Track totals across multiple sync calls (for batched hasMore syncs) |
| 102 | + let totalPulled = 0; |
| 103 | + let totalPushed = 0; |
| 104 | + let allWarnings: string[] = []; |
| 105 | + let batchCount = 0; |
| 106 | + const maxBatches = 50; // Safety limit to prevent infinite loops |
| 107 | +
|
99 | 108 | try { |
100 | | - const result = await api.triggerFullSync(); |
101 | | -
|
102 | | - if (result.success) { |
103 | | - const subsPulled = result.subscriptions?.pulledFromPds || 0; |
104 | | - const subsPushed = result.subscriptions?.pushedToPds || 0; |
105 | | - const readPulled = result.readPositions?.pulledFromPds || 0; |
106 | | - const readPushed = result.readPositions?.pushedToPds || 0; |
107 | | -
|
108 | | - syncSuccess = `Sync complete: ${subsPulled + readPulled} pulled, ${subsPushed + readPushed} pushed`; |
109 | | -
|
110 | | - // Show warnings if any |
111 | | - const warnings = [ |
112 | | - ...(result.subscriptions?.warnings || []), |
113 | | - ...(result.readPositions?.warnings || []), |
114 | | - ]; |
115 | | - if (warnings.length > 0) { |
116 | | - syncSuccess += `. Warning: ${warnings.join(', ')}`; |
| 109 | + let hasMore = true; |
| 110 | +
|
| 111 | + while (hasMore && batchCount < maxBatches) { |
| 112 | + batchCount++; |
| 113 | + if (batchCount > 1) { |
| 114 | + syncSuccess = `Syncing batch ${batchCount}...`; |
| 115 | + } |
| 116 | +
|
| 117 | + let result; |
| 118 | + try { |
| 119 | + result = await api.triggerFullSync(); |
| 120 | + } catch (error) { |
| 121 | + // Handle rate limit by waiting and retrying |
| 122 | + if (error instanceof RateLimitError) { |
| 123 | + const waitSeconds = Math.min(error.retryAfter, 300); // Cap at 5 minutes |
| 124 | + syncSuccess = `Rate limit reached. Waiting ${waitSeconds}s before continuing...`; |
| 125 | + await sleep(waitSeconds * 1000); |
| 126 | + // Retry this batch |
| 127 | + batchCount--; |
| 128 | + continue; |
| 129 | + } |
| 130 | + throw error; |
| 131 | + } |
| 132 | +
|
| 133 | + if (!result.success) { |
| 134 | + syncError = result.error || 'Sync failed'; |
| 135 | + return; |
117 | 136 | } |
118 | 137 |
|
119 | | - // Refresh sync status |
120 | | - const status = await api.getSyncStatus(); |
121 | | - lastSyncSubscriptions = status.lastSyncSubscriptions; |
122 | | - lastSyncReadPositions = status.lastSyncReadPositions; |
| 138 | + // Accumulate totals |
| 139 | + totalPulled += result.subscriptions?.pulledFromPds || 0; |
| 140 | + totalPushed += result.subscriptions?.pushedToPds || 0; |
123 | 141 |
|
124 | | - // Reload subscriptions to show any pulled items |
125 | | - await subscriptionsStore.load(); |
126 | | - } else { |
127 | | - syncError = result.error || 'Sync failed'; |
| 142 | + // Collect warnings |
| 143 | + allWarnings = [...allWarnings, ...(result.subscriptions?.warnings || [])]; |
| 144 | +
|
| 145 | + // Check if there's more to sync |
| 146 | + hasMore = result.hasMore || false; |
| 147 | + } |
| 148 | +
|
| 149 | + syncSuccess = `Sync complete: ${totalPulled} pulled, ${totalPushed} pushed`; |
| 150 | + if (batchCount > 1) { |
| 151 | + syncSuccess += ` (${batchCount} batches)`; |
| 152 | + } |
| 153 | +
|
| 154 | + // Show warnings if any |
| 155 | + if (allWarnings.length > 0) { |
| 156 | + syncSuccess += `. Warning: ${allWarnings.join(', ')}`; |
128 | 157 | } |
| 158 | +
|
| 159 | + // Refresh sync status |
| 160 | + const status = await api.getSyncStatus(); |
| 161 | + lastSyncSubscriptions = status.lastSyncSubscriptions; |
| 162 | +
|
| 163 | + // Reload subscriptions to show any pulled items |
| 164 | + await subscriptionsStore.load(); |
129 | 165 | } catch (error) { |
130 | 166 | console.error('Sync error:', error); |
131 | 167 | syncError = error instanceof Error ? error.message : 'Sync failed'; |
|
211 | 247 | <div class="sync-toggle-section"> |
212 | 248 | <label class="toggle-setting"> |
213 | 249 | <input type="checkbox" checked={pdsSyncEnabled} onchange={handleTogglePdsSync} /> |
214 | | - <span>Also sync subscriptions and reading data</span> |
| 250 | + <span>Also sync subscriptions</span> |
215 | 251 | </label> |
216 | 252 | <p class="setting-description"> |
217 | | - Optionally store your feed subscriptions and read/starred articles in your PDS. Note: this |
218 | | - data will be <strong>publicly visible</strong> on your PDS, but gives you full backup and portability. |
| 253 | + Optionally store your feed subscriptions in your PDS. Note: this data will be <strong |
| 254 | + >publicly visible</strong |
| 255 | + > on your PDS, but gives you full backup and portability. |
219 | 256 | </p> |
220 | 257 | </div> |
221 | 258 |
|
|
224 | 261 | <p class="sync-time"> |
225 | 262 | Subscriptions last synced: {formatSyncTime(lastSyncSubscriptions)} |
226 | 263 | </p> |
227 | | - <p class="sync-time">Reading data last synced: {formatSyncTime(lastSyncReadPositions)}</p> |
228 | 264 | </div> |
229 | 265 |
|
230 | 266 | <button class="btn btn-secondary" onclick={handleSync} disabled={isSyncing}> |
|
0 commit comments