Skip to content

Commit 990e39a

Browse files
committed
handle retrys and hasMore
1 parent d7a4ae1 commit 990e39a

File tree

2 files changed

+86
-40
lines changed

2 files changed

+86
-40
lines changed

src/lib/services/api.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ import type {
1010

1111
const API_BASE = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8787';
1212

13+
export class RateLimitError extends Error {
14+
retryAfter: number;
15+
16+
constructor(retryAfter: number) {
17+
super(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
18+
this.name = 'RateLimitError';
19+
this.retryAfter = retryAfter;
20+
}
21+
}
22+
1323
class ApiClient {
1424
private onUnauthorized: (() => void) | null = null;
1525

@@ -41,6 +51,12 @@ class ApiClient {
4151
throw new Error('Session expired');
4252
}
4353

54+
// Handle 429 - rate limit exceeded
55+
if (response.status === 429) {
56+
const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
57+
throw new RateLimitError(retryAfter);
58+
}
59+
4460
const error = await response.json().catch(() => ({ error: 'Request failed' }));
4561
throw new Error((error as { error: string }).error || `HTTP ${response.status}`);
4662
}
@@ -440,23 +456,17 @@ class ApiClient {
440456
pushedToPds: number;
441457
skipped: number;
442458
warnings: string[];
443-
};
444-
readPositions?: {
445-
success: boolean;
446-
pulledFromPds: number;
447-
pushedToPds: number;
448-
skipped: number;
449-
warnings: string[];
459+
hasMore?: boolean;
450460
};
451461
error?: string;
462+
hasMore?: boolean;
452463
}> {
453464
return this.fetch('/api/sync/full', { method: 'POST' });
454465
}
455466

456467
async getSyncStatus(): Promise<{
457468
pdsSyncEnabled: boolean;
458469
lastSyncSubscriptions: number | null;
459-
lastSyncReadPositions: number | null;
460470
}> {
461471
return this.fetch('/api/sync/status');
462472
}

src/routes/settings/+page.svelte

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import ImportOPMLModal from '$lib/components/ImportOPMLModal.svelte';
1212
import PageHeader from '$lib/components/common/PageHeader.svelte';
1313
import { downloadOPML } from '$lib/utils/opml-exporter';
14-
import { api } from '$lib/services/api';
14+
import { api, RateLimitError } from '$lib/services/api';
1515
1616
const fontOptions: { value: ArticleFont; label: string }[] = [
1717
{ value: 'sans-serif', label: 'Sans Serif' },
@@ -32,7 +32,6 @@
3232
// PDS Sync state
3333
let pdsSyncEnabled = $state(false);
3434
let lastSyncSubscriptions = $state<number | null>(null);
35-
let lastSyncReadPositions = $state<number | null>(null);
3635
let isSyncLoading = $state(false);
3736
let isSyncing = $state(false);
3837
let syncError = $state<string | null>(null);
@@ -58,7 +57,6 @@
5857
const settings = await api.getSettings();
5958
pdsSyncEnabled = settings.pdsSyncEnabled;
6059
lastSyncSubscriptions = settings.lastPdsSyncSubscriptions;
61-
lastSyncReadPositions = settings.lastPdsSyncReadPositions;
6260
} catch (error) {
6361
console.error('Failed to load sync settings:', error);
6462
} finally {
@@ -89,43 +87,81 @@
8987
}
9088
}
9189
90+
function sleep(ms: number): Promise<void> {
91+
return new Promise((resolve) => setTimeout(resolve, ms));
92+
}
93+
9294
async function handleSync() {
9395
if (isSyncing) return;
9496
9597
isSyncing = true;
9698
syncError = null;
9799
syncSuccess = null;
98100
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+
99108
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;
117136
}
118137
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;
123141
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(', ')}`;
128157
}
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();
129165
} catch (error) {
130166
console.error('Sync error:', error);
131167
syncError = error instanceof Error ? error.message : 'Sync failed';
@@ -211,11 +247,12 @@
211247
<div class="sync-toggle-section">
212248
<label class="toggle-setting">
213249
<input type="checkbox" checked={pdsSyncEnabled} onchange={handleTogglePdsSync} />
214-
<span>Also sync subscriptions and reading data</span>
250+
<span>Also sync subscriptions</span>
215251
</label>
216252
<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.
219256
</p>
220257
</div>
221258

@@ -224,7 +261,6 @@
224261
<p class="sync-time">
225262
Subscriptions last synced: {formatSyncTime(lastSyncSubscriptions)}
226263
</p>
227-
<p class="sync-time">Reading data last synced: {formatSyncTime(lastSyncReadPositions)}</p>
228264
</div>
229265

230266
<button class="btn btn-secondary" onclick={handleSync} disabled={isSyncing}>

0 commit comments

Comments
 (0)