UNPKG

@omnistreamai/data-core

Version:

189 lines (151 loc) 5.45 kB
--- name: offline-sync description: > Handle offline sync failures. Use PendingQueue for failed operations, configure RetryPolicy with exponential backoff, call getPendingCount and syncPending to manage retry. Understand which errors are retryable (429, 5xx) vs non-retryable (405). Use onSyncFailed and onSyncSuccess callbacks. type: core library: '@omnistreamai/data-sync' library_version: '0.4.1' sources: - 'omni-stream-ai/data-sync:packages/core/src/types.ts' - 'omni-stream-ai/data-sync:packages/core/src/pending-queue.ts' - 'omni-stream-ai/data-sync:packages/core/src/store.ts' --- # Offline Sync Handle network failures and sync retries when operations fail. ## Setup ```typescript import { createSyncManager } from '@omnistreamai/data-core'; import { DEFAULT_RETRY_POLICY } from '@omnistreamai/data-core'; const syncManager = createSyncManager({ localAdapter: new IndexedDBAdapter({ dbName: 'app', version: 1 }), remoteAdapter: new WebDAVAdapter(), notThrowLocalError: true, // Required for sync callbacks to work }); const store = syncManager.createStore<Todo>('todos', { idKey: 'id', syncConfig: { defaultPolicy: { maxRetries: 3, baseDelayMs: 5000, backoff: 'exponential', jitter: true, retryableStatuses: [408, 429, 500, 502, 503, 504], }, autoSync: false, // Manual sync only }, syncCallbacks: { onSyncFailed: ({ item, error, attempt, isMaxRetriesExceeded, nextRetryDelayMs }) => { console.error(`Sync failed for ${item.recordId}: ${error.message}`, { attempt, isMaxRetriesExceeded, nextRetryDelayMs, }); }, onSyncSuccess: (item) => { console.log(`Synced ${item.recordId} (${item.operation})`); }, }, }); ``` ## Core Patterns ### Check pending sync count ```typescript const pendingCount = await store.getPendingCount(); console.log(`${pendingCount} operations waiting to sync`); ``` ### Manually sync pending items ```typescript // Call after coming back online const result = await store.syncPending(); console.log(`Synced: ${result.success}, Failed: ${result.failed}`); // syncPending returns items that are past their retry delay // Retries follow exponential backoff: attempt 1 = immediate, attempt 2+ = baseDelay * 2^(n-1) ``` ### Understand retryable vs non-retryable errors ```typescript // These errors ARE queued for retry: // - 408 Request Timeout // - 429 Too Many Requests // - 500, 502, 503, 504 Server errors // - ECONNREFUSED, ENOTFOUND, ETIMEDOUT, 'timeout', 'network', 'fetch failed' // This error is NEVER queued: // - 405 Method Not Allowed — operation not supported by server, retrying won't help ``` ### Configure retry policy ```typescript const store = syncManager.createStore<Todo>('todos', { idKey: 'id', syncConfig: { defaultPolicy: { maxRetries: 5, baseDelayMs: 10000, // 10 seconds base maxDelayMs: 300000, // 5 minutes max backoff: 'exponential', // 10s → 20s → 40s → 80s → 160s jitter: true, // Random 0-50% added to prevent thundering herd retryableStatuses: [429, 500, 502, 503, 504], retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT', 'network'], }, }, }); ``` ### Retry flow behavior ```typescript // When add/update/delete fails with retryable error: // 1. Item added to pending queue with attempts=1 // 2. onSyncFailed called with isMaxRetriesExceeded=false // 3. When syncPending() is called and delay has passed: // - If attempts < maxRetries: retry operation, attempts++ // - If attempts >= maxRetries: mark failed, onSyncFailed with isMaxRetriesExceeded=true ``` ## Common Mistakes ### HIGH Assume all errors are retried Wrong: ```typescript // Assuming every failed operation will be retried automatically await store.add({ id: '1', title: 'Test' }); // If this fails with 405, it might be lost — 405 is NOT queued ``` Correct: ```typescript // 405 Method Not Allowed is NOT queued — understand which errors retry // 405 means the server doesn't support this operation — no point retrying // Only these are queued: // - 429 Too Many Requests — wait and retry // - 500/502/503/504 — server issue, retry may succeed // Always monitor with onSyncFailed callback: syncCallbacks: { onSyncFailed: ({ item, isMaxRetriesExceeded }) => { if (isMaxRetriesExceeded) { // Item permanently failed — notify user or persist to dead-letter queue console.error(`Permanent sync failure for ${item.recordId}: ${item.lastError}`); } }, }, ``` Source: packages/core/src/pending-queue.ts:44-58 --- ### HIGH Not using notThrowLocalError with syncCallbacks Wrong: ```typescript const syncManager = createSyncManager({ localAdapter: new IndexedDBAdapter(), remoteAdapter: new WebDAVAdapter(), // notThrowLocalError defaults to false // When a local write succeeds but remote fails, error propagates // syncCallbacks.onSyncFailed will NOT fire }); ``` Correct: ```typescript const syncManager = createSyncManager({ localAdapter: new IndexedDBAdapter(), remoteAdapter: new WebDAVAdapter(), notThrowLocalError: true, // ✅ Required for sync callbacks to work }); ``` `notThrowLocalError` must be `true` for `onSyncFailed` and `onSyncSuccess` callbacks to fire. When `false`, local write errors propagate and sync callbacks are skipped. Source: packages/core/src/store.ts:76-83 See also: data-sync/initialize-sync-system