@pol-studios/powersync 1.0.4 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CacheSettingsManager-1exbOC6S.d.ts +261 -0
- package/dist/attachments/index.d.ts +65 -355
- package/dist/attachments/index.js +24 -6
- package/dist/{types-Cd7RhNqf.d.ts → background-sync-ChCXW-EV.d.ts} +53 -2
- package/dist/chunk-4C3RY5SU.js +204 -0
- package/dist/chunk-4C3RY5SU.js.map +1 -0
- package/dist/{chunk-3AYXHQ4W.js → chunk-53WH2JJV.js} +111 -47
- package/dist/chunk-53WH2JJV.js.map +1 -0
- package/dist/chunk-A4IBBWGO.js +377 -0
- package/dist/chunk-A4IBBWGO.js.map +1 -0
- package/dist/chunk-BREGB4WL.js +1768 -0
- package/dist/chunk-BREGB4WL.js.map +1 -0
- package/dist/{chunk-EJ23MXPQ.js → chunk-CGL33PL4.js} +3 -1
- package/dist/chunk-CGL33PL4.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DHYUBVP7.js +131 -0
- package/dist/chunk-DHYUBVP7.js.map +1 -0
- package/dist/chunk-FV2HXEIY.js +124 -0
- package/dist/chunk-FV2HXEIY.js.map +1 -0
- package/dist/chunk-GKF7TOMT.js +1 -0
- package/dist/{chunk-OTJXIRWX.js → chunk-H772V6XQ.js} +304 -51
- package/dist/chunk-H772V6XQ.js.map +1 -0
- package/dist/{chunk-C2RSTGDC.js → chunk-HFOFLW5F.js} +525 -87
- package/dist/chunk-HFOFLW5F.js.map +1 -0
- package/dist/chunk-KGSFAE5B.js +1 -0
- package/dist/chunk-LNL64IJZ.js +1 -0
- package/dist/chunk-MKD2VCX3.js +32 -0
- package/dist/chunk-MKD2VCX3.js.map +1 -0
- package/dist/{chunk-7EMDVIZX.js → chunk-N75DEF5J.js} +19 -1
- package/dist/chunk-N75DEF5J.js.map +1 -0
- package/dist/chunk-P6WOZO7H.js +49 -0
- package/dist/chunk-P6WOZO7H.js.map +1 -0
- package/dist/chunk-TGBT5XBE.js +1 -0
- package/dist/chunk-TGBT5XBE.js.map +1 -0
- package/dist/chunk-UEYRTLKE.js +72 -0
- package/dist/chunk-UEYRTLKE.js.map +1 -0
- package/dist/chunk-WGHNIAF7.js +329 -0
- package/dist/chunk-WGHNIAF7.js.map +1 -0
- package/dist/chunk-WQ5MPAVC.js +449 -0
- package/dist/chunk-WQ5MPAVC.js.map +1 -0
- package/dist/{chunk-FPTDATY5.js → chunk-XQAJM2MW.js} +22 -11
- package/dist/chunk-XQAJM2MW.js.map +1 -0
- package/dist/chunk-YSTEESEG.js +676 -0
- package/dist/chunk-YSTEESEG.js.map +1 -0
- package/dist/chunk-ZEOKPWUC.js +1165 -0
- package/dist/chunk-ZEOKPWUC.js.map +1 -0
- package/dist/connector/index.d.ts +182 -2
- package/dist/connector/index.js +14 -3
- package/dist/core/index.d.ts +5 -3
- package/dist/core/index.js +5 -2
- package/dist/error/index.d.ts +54 -0
- package/dist/error/index.js +8 -0
- package/dist/error/index.js.map +1 -0
- package/dist/index.d.ts +237 -11
- package/dist/index.js +183 -27
- package/dist/index.native.d.ts +20 -9
- package/dist/index.native.js +183 -28
- package/dist/index.web.d.ts +20 -9
- package/dist/index.web.js +184 -28
- package/dist/maintenance/index.d.ts +118 -0
- package/dist/maintenance/index.js +17 -0
- package/dist/maintenance/index.js.map +1 -0
- package/dist/platform/index.d.ts +16 -1
- package/dist/platform/index.js +2 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.d.ts +2 -2
- package/dist/platform/index.native.js +2 -1
- package/dist/platform/index.web.d.ts +1 -1
- package/dist/platform/index.web.js +2 -1
- package/dist/pol-attachment-queue-C7YNXXhK.d.ts +676 -0
- package/dist/provider/index.d.ts +693 -12
- package/dist/provider/index.js +57 -12
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.js +28 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.native.d.ts +6 -0
- package/dist/storage/index.native.js +26 -0
- package/dist/storage/index.native.js.map +1 -0
- package/dist/storage/index.web.d.ts +6 -0
- package/dist/storage/index.web.js +26 -0
- package/dist/storage/index.web.js.map +1 -0
- package/dist/storage/upload/index.d.ts +55 -0
- package/dist/storage/upload/index.js +15 -0
- package/dist/storage/upload/index.js.map +1 -0
- package/dist/storage/upload/index.native.d.ts +57 -0
- package/dist/storage/upload/index.native.js +14 -0
- package/dist/storage/upload/index.native.js.map +1 -0
- package/dist/storage/upload/index.web.d.ts +5 -0
- package/dist/storage/upload/index.web.js +14 -0
- package/dist/storage/upload/index.web.js.map +1 -0
- package/dist/{index-Cb-NI0Ct.d.ts → supabase-connector-qLm-WHkM.d.ts} +146 -10
- package/dist/sync/index.d.ts +288 -22
- package/dist/sync/index.js +23 -5
- package/dist/types-BVacP54t.d.ts +52 -0
- package/dist/types-Bgvx7-E8.d.ts +187 -0
- package/dist/{types-afHtE1U_.d.ts → types-CDqWh56B.d.ts} +2 -0
- package/package.json +72 -2
- package/dist/chunk-32OLICZO.js +0 -1
- package/dist/chunk-3AYXHQ4W.js.map +0 -1
- package/dist/chunk-7EMDVIZX.js.map +0 -1
- package/dist/chunk-7JQZBZ5N.js +0 -1
- package/dist/chunk-C2RSTGDC.js.map +0 -1
- package/dist/chunk-EJ23MXPQ.js.map +0 -1
- package/dist/chunk-FPTDATY5.js.map +0 -1
- package/dist/chunk-GMFDCVMZ.js +0 -1285
- package/dist/chunk-GMFDCVMZ.js.map +0 -1
- package/dist/chunk-OLHGI472.js +0 -1
- package/dist/chunk-OTJXIRWX.js.map +0 -1
- package/dist/chunk-V6LJ6MR2.js +0 -740
- package/dist/chunk-V6LJ6MR2.js.map +0 -1
- package/dist/chunk-VJCL2SWD.js +0 -1
- /package/dist/{chunk-32OLICZO.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-7JQZBZ5N.js.map → chunk-GKF7TOMT.js.map} +0 -0
- /package/dist/{chunk-OLHGI472.js.map → chunk-KGSFAE5B.js.map} +0 -0
- /package/dist/{chunk-VJCL2SWD.js.map → chunk-LNL64IJZ.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/retry.ts"],"sourcesContent":["/**\n * Exponential backoff retry utilities for resilient network operations.\n *\n * Provides configurable retry logic with exponential backoff, jitter,\n * and abort signal support for cancellation.\n */\n\n/**\n * Configuration for exponential backoff retry behavior.\n */\nexport interface BackoffConfig {\n /** Maximum number of retry attempts (0 = no retries, just one attempt) */\n maxRetries: number;\n /** Base delay in milliseconds before first retry */\n baseDelayMs: number;\n /** Maximum delay cap in milliseconds */\n maxDelayMs: number;\n /** Multiplier applied to delay for each subsequent retry (typically 2) */\n backoffMultiplier: number;\n}\n\n/**\n * Options for the withExponentialBackoff function.\n */\nexport interface BackoffOptions {\n /** Optional AbortSignal for cancellation support */\n signal?: AbortSignal;\n /** Callback invoked before each retry attempt */\n onRetry?: (attempt: number, delay: number, error: Error) => void;\n}\n\n/**\n * Error thrown when an operation is aborted via AbortSignal.\n */\nexport class AbortError extends Error {\n constructor(message = 'Operation aborted') {\n super(message);\n this.name = 'AbortError';\n }\n}\n\n/**\n * Error thrown when all retry attempts have been exhausted.\n */\nexport class RetryExhaustedError extends Error {\n /** The last error that caused the final retry to fail */\n readonly cause: Error;\n /** Total number of attempts made */\n readonly attempts: number;\n constructor(cause: Error, attempts: number) {\n super(`Retry exhausted after ${attempts} attempt(s): ${cause.message}`);\n this.name = 'RetryExhaustedError';\n this.cause = cause;\n this.attempts = attempts;\n }\n}\n\n/**\n * Calculates the delay for a given retry attempt using exponential backoff.\n *\n * Formula: min(baseDelay * (multiplier ^ attempt), maxDelay)\n *\n * @param attempt - The current attempt number (0-indexed)\n * @param config - Backoff configuration\n * @returns Delay in milliseconds (without jitter)\n *\n * @example\n * ```ts\n * const config = { baseDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2 };\n * calculateBackoffDelay(0, config); // 1000\n * calculateBackoffDelay(1, config); // 2000\n * calculateBackoffDelay(2, config); // 4000\n * calculateBackoffDelay(5, config); // 30000 (capped at maxDelayMs)\n * ```\n */\nexport function calculateBackoffDelay(attempt: number, config: Pick<BackoffConfig, 'baseDelayMs' | 'maxDelayMs' | 'backoffMultiplier'>): number {\n const {\n baseDelayMs,\n maxDelayMs,\n backoffMultiplier\n } = config;\n\n // Ensure non-negative attempt number\n const safeAttempt = Math.max(0, attempt);\n\n // Calculate exponential delay\n const exponentialDelay = baseDelayMs * Math.pow(backoffMultiplier, safeAttempt);\n\n // Cap at maxDelayMs\n return Math.min(exponentialDelay, maxDelayMs);\n}\n\n/**\n * Adds jitter (±10%) to a delay value to prevent thundering herd.\n *\n * @param delay - Base delay in milliseconds\n * @returns Delay with random jitter applied\n */\nexport function addJitter(delay: number): number {\n // Generate random factor between 0.9 and 1.1 (±10%)\n const jitterFactor = 0.9 + Math.random() * 0.2;\n return Math.round(delay * jitterFactor);\n}\n\n/**\n * Sleep utility that respects AbortSignal for cancellation.\n *\n * @param ms - Duration to sleep in milliseconds\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves after the delay or rejects if aborted\n * @throws {AbortError} If the signal is aborted during sleep\n *\n * @example\n * ```ts\n * const controller = new AbortController();\n *\n * // Sleep for 1 second\n * await sleep(1000);\n *\n * // Sleep with cancellation support\n * await sleep(1000, controller.signal);\n *\n * // Cancel the sleep\n * controller.abort();\n * ```\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n // Check if already aborted\n if (signal?.aborted) {\n reject(new AbortError());\n return;\n }\n\n // Handle zero or negative duration\n if (ms <= 0) {\n resolve();\n return;\n }\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const handleAbort = () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n reject(new AbortError());\n };\n\n // Set up abort listener if signal provided\n if (signal) {\n signal.addEventListener('abort', handleAbort, {\n once: true\n });\n }\n timeoutId = setTimeout(() => {\n // Clean up abort listener\n if (signal) {\n signal.removeEventListener('abort', handleAbort);\n }\n resolve();\n }, ms);\n });\n}\n\n/**\n * Default backoff configuration suitable for most network operations.\n */\nexport const DEFAULT_BACKOFF_CONFIG: BackoffConfig = {\n maxRetries: 3,\n baseDelayMs: 1000,\n maxDelayMs: 30000,\n backoffMultiplier: 2\n};\n\n/**\n * Executes a function with exponential backoff retry logic.\n *\n * Retries the provided function on failure using exponential backoff with jitter.\n * Supports cancellation via AbortSignal and provides hooks for retry observation.\n *\n * @param fn - Async function to execute with retry logic\n * @param config - Backoff configuration (maxRetries, delays, multiplier)\n * @param options - Optional abort signal and retry callback\n * @returns Promise resolving to the function result\n * @throws {AbortError} If operation is aborted via signal\n * @throws {RetryExhaustedError} If all retry attempts fail\n *\n * @example\n * ```ts\n * const controller = new AbortController();\n *\n * const result = await withExponentialBackoff(\n * async () => {\n * const response = await fetch('https://api.example.com/data');\n * if (!response.ok) throw new Error('Request failed');\n * return response.json();\n * },\n * {\n * maxRetries: 3,\n * baseDelayMs: 1000,\n * maxDelayMs: 30000,\n * backoffMultiplier: 2,\n * },\n * {\n * signal: controller.signal,\n * onRetry: (attempt, delay, error) => {\n * console.log(`Retry ${attempt} in ${delay}ms: ${error.message}`);\n * },\n * }\n * );\n * ```\n */\nexport async function withExponentialBackoff<T>(fn: () => Promise<T>, config: BackoffConfig, options?: BackoffOptions): Promise<T> {\n const {\n maxRetries,\n baseDelayMs,\n maxDelayMs,\n backoffMultiplier\n } = config;\n const {\n signal,\n onRetry\n } = options ?? {};\n\n // Check for immediate abort\n if (signal?.aborted) {\n throw new AbortError();\n }\n\n // Validate config\n const safeMaxRetries = Math.max(0, Math.floor(maxRetries));\n const totalAttempts = safeMaxRetries + 1; // Initial attempt + retries\n\n let lastError: Error | undefined;\n for (let attempt = 0; attempt < totalAttempts; attempt++) {\n // Check for abort before each attempt\n if (signal?.aborted) {\n throw new AbortError();\n }\n try {\n return await fn();\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Check if this was the last attempt\n const isLastAttempt = attempt === totalAttempts - 1;\n if (isLastAttempt) {\n break;\n }\n\n // Check for abort before sleeping\n if (signal?.aborted) {\n throw new AbortError();\n }\n\n // Calculate delay with jitter\n const baseDelay = calculateBackoffDelay(attempt, {\n baseDelayMs,\n maxDelayMs,\n backoffMultiplier\n });\n const delayWithJitter = addJitter(baseDelay);\n\n // Notify about upcoming retry\n onRetry?.(attempt + 1, delayWithJitter, lastError);\n\n // Wait before retry\n await sleep(delayWithJitter, signal);\n }\n }\n\n // All attempts exhausted\n throw new RetryExhaustedError(lastError!, totalAttempts);\n}"],"mappings":";AAkCO,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YAAY,UAAU,qBAAqB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,sBAAN,cAAkC,MAAM;AAAA;AAAA,EAEpC;AAAA;AAAA,EAEA;AAAA,EACT,YAAY,OAAc,UAAkB;AAC1C,UAAM,yBAAyB,QAAQ,gBAAgB,MAAM,OAAO,EAAE;AACtE,SAAK,OAAO;AACZ,SAAK,QAAQ;AACb,SAAK,WAAW;AAAA,EAClB;AACF;AAoBO,SAAS,sBAAsB,SAAiB,QAAyF;AAC9I,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO;AAGvC,QAAM,mBAAmB,cAAc,KAAK,IAAI,mBAAmB,WAAW;AAG9E,SAAO,KAAK,IAAI,kBAAkB,UAAU;AAC9C;AAQO,SAAS,UAAU,OAAuB;AAE/C,QAAM,eAAe,MAAM,KAAK,OAAO,IAAI;AAC3C,SAAO,KAAK,MAAM,QAAQ,YAAY;AACxC;AAwBO,SAAS,MAAM,IAAY,QAAqC;AACrE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEtC,QAAI,QAAQ,SAAS;AACnB,aAAO,IAAI,WAAW,CAAC;AACvB;AAAA,IACF;AAGA,QAAI,MAAM,GAAG;AACX,cAAQ;AACR;AAAA,IACF;AACA,QAAI;AACJ,UAAM,cAAc,MAAM;AACxB,UAAI,cAAc,QAAW;AAC3B,qBAAa,SAAS;AAAA,MACxB;AACA,aAAO,IAAI,WAAW,CAAC;AAAA,IACzB;AAGA,QAAI,QAAQ;AACV,aAAO,iBAAiB,SAAS,aAAa;AAAA,QAC5C,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AACA,gBAAY,WAAW,MAAM;AAE3B,UAAI,QAAQ;AACV,eAAO,oBAAoB,SAAS,WAAW;AAAA,MACjD;AACA,cAAQ;AAAA,IACV,GAAG,EAAE;AAAA,EACP,CAAC;AACH;AAKO,IAAM,yBAAwC;AAAA,EACnD,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,mBAAmB;AACrB;AAwCA,eAAsB,uBAA0B,IAAsB,QAAuB,SAAsC;AACjI,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,EACF,IAAI,WAAW,CAAC;AAGhB,MAAI,QAAQ,SAAS;AACnB,UAAM,IAAI,WAAW;AAAA,EACvB;AAGA,QAAM,iBAAiB,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,CAAC;AACzD,QAAM,gBAAgB,iBAAiB;AAEvC,MAAI;AACJ,WAAS,UAAU,GAAG,UAAU,eAAe,WAAW;AAExD,QAAI,QAAQ,SAAS;AACnB,YAAM,IAAI,WAAW;AAAA,IACvB;AACA,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,OAAO;AACd,kBAAY,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAGpE,YAAM,gBAAgB,YAAY,gBAAgB;AAClD,UAAI,eAAe;AACjB;AAAA,MACF;AAGA,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,WAAW;AAAA,MACvB;AAGA,YAAM,YAAY,sBAAsB,SAAS;AAAA,QAC/C;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,kBAAkB,UAAU,SAAS;AAG3C,gBAAU,UAAU,GAAG,iBAAiB,SAAS;AAGjD,YAAM,MAAM,iBAAiB,MAAM;AAAA,IACrC;AAAA,EACF;AAGA,QAAM,IAAI,oBAAoB,WAAY,aAAa;AACzD;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-GKF7TOMT.js.map
|
|
@@ -4,14 +4,15 @@ import {
|
|
|
4
4
|
LATENCY_DEGRADED_THRESHOLD_MS,
|
|
5
5
|
MAX_CONSECUTIVE_FAILURES,
|
|
6
6
|
STATUS_NOTIFY_THROTTLE_MS,
|
|
7
|
+
STORAGE_KEY_AUTO_OFFLINE,
|
|
7
8
|
STORAGE_KEY_METRICS,
|
|
8
9
|
STORAGE_KEY_PAUSED,
|
|
9
10
|
STORAGE_KEY_SYNC_MODE
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-CGL33PL4.js";
|
|
11
12
|
import {
|
|
12
13
|
classifyError,
|
|
13
14
|
generateFailureId
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XQAJM2MW.js";
|
|
15
16
|
|
|
16
17
|
// src/provider/types.ts
|
|
17
18
|
var DEFAULT_SYNC_STATUS = {
|
|
@@ -51,6 +52,8 @@ var DEFAULT_SYNC_CONFIG = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
// src/sync/status-tracker.ts
|
|
55
|
+
var STORAGE_KEY_COMPLETED_TRANSACTIONS = "@pol-powersync:completed_transactions";
|
|
56
|
+
var STORAGE_KEY_FAILED_TRANSACTIONS = "@pol-powersync:failed_transactions";
|
|
54
57
|
var SyncStatusTracker = class _SyncStatusTracker {
|
|
55
58
|
storage;
|
|
56
59
|
logger;
|
|
@@ -64,6 +67,13 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
64
67
|
_syncModeListeners = /* @__PURE__ */ new Set();
|
|
65
68
|
// Force next upload flag for "Sync Now" functionality
|
|
66
69
|
_forceNextUpload = false;
|
|
70
|
+
// Network reachability gate - blocks uploads instantly when network is unreachable
|
|
71
|
+
_networkReachable = true;
|
|
72
|
+
_networkRestoreTimer = null;
|
|
73
|
+
_networkRestoreDelayMs = 1500;
|
|
74
|
+
// 1.5 seconds delay before restoring
|
|
75
|
+
// Debounce timer for persist operations to avoid race conditions
|
|
76
|
+
_persistDebounceTimer = null;
|
|
67
77
|
// Track download progress separately to preserve it when offline
|
|
68
78
|
_lastProgress = null;
|
|
69
79
|
// Failed transaction tracking
|
|
@@ -72,10 +82,15 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
72
82
|
_failureTTLMs = 24 * 60 * 60 * 1e3;
|
|
73
83
|
// 24 hours
|
|
74
84
|
_failureListeners = /* @__PURE__ */ new Set();
|
|
75
|
-
// Completed transaction tracking
|
|
85
|
+
// Completed transaction tracking (no limit - full audit trail)
|
|
76
86
|
_completedTransactions = [];
|
|
77
|
-
_maxCompletedHistory = 20;
|
|
78
87
|
_completedListeners = /* @__PURE__ */ new Set();
|
|
88
|
+
// Track when notifications were last displayed/dismissed for "auto-dismiss on display"
|
|
89
|
+
// This allows filtering completed transactions to only show new ones since last display
|
|
90
|
+
_lastNotificationTime = Date.now();
|
|
91
|
+
// Auto-offline flag: tracks whether offline mode was set automatically (network loss)
|
|
92
|
+
// vs manually (user chose offline). Persisted so auto-restore works after app restart.
|
|
93
|
+
_isAutoOffline = false;
|
|
79
94
|
constructor(storage, logger, options = {}) {
|
|
80
95
|
this.storage = storage;
|
|
81
96
|
this.logger = logger;
|
|
@@ -114,6 +129,53 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
114
129
|
} catch (err) {
|
|
115
130
|
this.logger.warn("[StatusTracker] Failed to load sync mode:", err);
|
|
116
131
|
}
|
|
132
|
+
try {
|
|
133
|
+
const autoOfflineValue = await this.storage.getItem(STORAGE_KEY_AUTO_OFFLINE);
|
|
134
|
+
this._isAutoOffline = autoOfflineValue === "true";
|
|
135
|
+
this.logger.debug("[StatusTracker] Loaded isAutoOffline:", this._isAutoOffline);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.logger.warn("[StatusTracker] Failed to load auto-offline flag:", err);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const completedJson = await this.storage.getItem(STORAGE_KEY_COMPLETED_TRANSACTIONS);
|
|
141
|
+
if (completedJson) {
|
|
142
|
+
const parsed = JSON.parse(completedJson);
|
|
143
|
+
this._completedTransactions = parsed.map((item) => {
|
|
144
|
+
const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
|
|
145
|
+
return {
|
|
146
|
+
...item,
|
|
147
|
+
completedAt: new Date(item.completedAt),
|
|
148
|
+
entries: remappedEntries
|
|
149
|
+
};
|
|
150
|
+
}).filter((item) => !isNaN(item.completedAt.getTime()) && item.entries.length > 0);
|
|
151
|
+
this.logger.debug("[StatusTracker] Loaded", this._completedTransactions.length, "completed transactions");
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
this.logger.warn("[StatusTracker] Failed to load completed transactions:", err);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const failedJson = await this.storage.getItem(STORAGE_KEY_FAILED_TRANSACTIONS);
|
|
158
|
+
if (failedJson) {
|
|
159
|
+
const parsed = JSON.parse(failedJson);
|
|
160
|
+
this._failedTransactions = parsed.map((item) => {
|
|
161
|
+
const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
|
|
162
|
+
return {
|
|
163
|
+
...item,
|
|
164
|
+
firstFailedAt: new Date(item.firstFailedAt),
|
|
165
|
+
lastFailedAt: new Date(item.lastFailedAt),
|
|
166
|
+
error: {
|
|
167
|
+
...item.error,
|
|
168
|
+
timestamp: new Date(item.error.timestamp)
|
|
169
|
+
},
|
|
170
|
+
entries: remappedEntries
|
|
171
|
+
};
|
|
172
|
+
}).filter((item) => !isNaN(item.firstFailedAt.getTime()) && !isNaN(item.lastFailedAt.getTime()) && item.entries.length > 0);
|
|
173
|
+
this.logger.debug("[StatusTracker] Loaded", this._failedTransactions.length, "failed transactions");
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.logger.warn("[StatusTracker] Failed to load failed transactions:", err);
|
|
177
|
+
}
|
|
178
|
+
this.cleanupStaleFailures();
|
|
117
179
|
}
|
|
118
180
|
/**
|
|
119
181
|
* Dispose the tracker and clear timers.
|
|
@@ -123,6 +185,14 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
123
185
|
clearTimeout(this._notifyTimer);
|
|
124
186
|
this._notifyTimer = null;
|
|
125
187
|
}
|
|
188
|
+
if (this._persistDebounceTimer) {
|
|
189
|
+
clearTimeout(this._persistDebounceTimer);
|
|
190
|
+
this._persistDebounceTimer = null;
|
|
191
|
+
}
|
|
192
|
+
if (this._networkRestoreTimer) {
|
|
193
|
+
clearTimeout(this._networkRestoreTimer);
|
|
194
|
+
this._networkRestoreTimer = null;
|
|
195
|
+
}
|
|
126
196
|
this._listeners.clear();
|
|
127
197
|
this._syncModeListeners.clear();
|
|
128
198
|
this._failureListeners.clear();
|
|
@@ -156,17 +226,10 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
156
226
|
return this._state.syncMode;
|
|
157
227
|
}
|
|
158
228
|
/**
|
|
159
|
-
*
|
|
160
|
-
* @deprecated Use getSyncMode() instead
|
|
161
|
-
*/
|
|
162
|
-
isPaused() {
|
|
163
|
-
return this._state.syncMode === "offline";
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Check if uploads are allowed based on current sync mode.
|
|
229
|
+
* Check if uploads are allowed based on current sync mode and network reachability.
|
|
167
230
|
*/
|
|
168
231
|
canUpload() {
|
|
169
|
-
return this._state.syncMode === "push-pull";
|
|
232
|
+
return this._networkReachable && this._state.syncMode === "push-pull";
|
|
170
233
|
}
|
|
171
234
|
/**
|
|
172
235
|
* Check if downloads are allowed based on current sync mode.
|
|
@@ -192,7 +255,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
192
255
|
}
|
|
193
256
|
}
|
|
194
257
|
/**
|
|
195
|
-
* Check if upload should proceed, considering force flag.
|
|
258
|
+
* Check if upload should proceed, considering force flag and network reachability.
|
|
196
259
|
* NOTE: Does NOT auto-reset the flag - caller must use clearForceNextUpload()
|
|
197
260
|
* after all uploads are complete. This prevents race conditions when
|
|
198
261
|
* PowerSync calls uploadData() multiple times for multiple transactions.
|
|
@@ -201,8 +264,43 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
201
264
|
if (this._forceNextUpload) {
|
|
202
265
|
return true;
|
|
203
266
|
}
|
|
267
|
+
if (!this._networkReachable) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
204
270
|
return this._state.syncMode === "push-pull";
|
|
205
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Set network reachability state.
|
|
274
|
+
* - When unreachable: Instantly blocks uploads (0ms)
|
|
275
|
+
* - When reachable: Delayed restore (1-2 seconds) to avoid flickering on brief disconnects
|
|
276
|
+
*/
|
|
277
|
+
setNetworkReachable(reachable) {
|
|
278
|
+
if (this._networkRestoreTimer) {
|
|
279
|
+
clearTimeout(this._networkRestoreTimer);
|
|
280
|
+
this._networkRestoreTimer = null;
|
|
281
|
+
}
|
|
282
|
+
if (!reachable) {
|
|
283
|
+
if (this._networkReachable) {
|
|
284
|
+
this._networkReachable = false;
|
|
285
|
+
this.logger.debug("[StatusTracker] Network unreachable - uploads blocked instantly");
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
if (!this._networkReachable) {
|
|
289
|
+
this.logger.debug("[StatusTracker] Network reachable - scheduling delayed restore");
|
|
290
|
+
this._networkRestoreTimer = setTimeout(() => {
|
|
291
|
+
this._networkRestoreTimer = null;
|
|
292
|
+
this._networkReachable = true;
|
|
293
|
+
this.logger.debug("[StatusTracker] Network restored - uploads enabled");
|
|
294
|
+
}, this._networkRestoreDelayMs);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get current network reachability state.
|
|
300
|
+
*/
|
|
301
|
+
isNetworkReachable() {
|
|
302
|
+
return this._networkReachable;
|
|
303
|
+
}
|
|
206
304
|
/**
|
|
207
305
|
* Get pending mutations.
|
|
208
306
|
*/
|
|
@@ -290,12 +388,27 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
290
388
|
}
|
|
291
389
|
this._notifyListeners(true);
|
|
292
390
|
}
|
|
391
|
+
// ─── Auto-Offline Management ──────────────────────────────────────────────
|
|
293
392
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
393
|
+
* Get whether offline mode was set automatically (network loss) vs manually.
|
|
394
|
+
* Used to determine if sync should auto-resume when network returns.
|
|
296
395
|
*/
|
|
297
|
-
|
|
298
|
-
|
|
396
|
+
getIsAutoOffline() {
|
|
397
|
+
return this._isAutoOffline;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Set the auto-offline flag and persist it.
|
|
401
|
+
* @param isAuto - true if offline was set automatically, false if user chose offline
|
|
402
|
+
*/
|
|
403
|
+
async setIsAutoOffline(isAuto) {
|
|
404
|
+
if (this._isAutoOffline === isAuto) return;
|
|
405
|
+
this._isAutoOffline = isAuto;
|
|
406
|
+
try {
|
|
407
|
+
await this.storage.setItem(STORAGE_KEY_AUTO_OFFLINE, isAuto ? "true" : "false");
|
|
408
|
+
this.logger.debug("[StatusTracker] Auto-offline flag changed:", isAuto);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
this.logger.warn("[StatusTracker] Failed to persist auto-offline flag:", err);
|
|
411
|
+
}
|
|
299
412
|
}
|
|
300
413
|
// ─── Subscriptions ─────────────────────────────────────────────────────────
|
|
301
414
|
/**
|
|
@@ -320,30 +433,18 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
320
433
|
this._syncModeListeners.delete(listener);
|
|
321
434
|
};
|
|
322
435
|
}
|
|
323
|
-
/**
|
|
324
|
-
* Subscribe to paused state changes.
|
|
325
|
-
* @deprecated Use onSyncModeChange() instead
|
|
326
|
-
* @returns Unsubscribe function
|
|
327
|
-
*/
|
|
328
|
-
onPausedChange(listener) {
|
|
329
|
-
const wrappedListener = (mode) => {
|
|
330
|
-
listener(mode === "offline");
|
|
331
|
-
};
|
|
332
|
-
this._syncModeListeners.add(wrappedListener);
|
|
333
|
-
listener(this._state.syncMode === "offline");
|
|
334
|
-
return () => {
|
|
335
|
-
this._syncModeListeners.delete(wrappedListener);
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
436
|
// ─── Failed Transaction Tracking ────────────────────────────────────────────
|
|
339
437
|
/**
|
|
340
438
|
* Record a transaction failure.
|
|
341
439
|
* If a failure for the same entries already exists, updates the retry count.
|
|
342
440
|
* Otherwise, creates a new failure record.
|
|
441
|
+
*
|
|
442
|
+
* @param preserveMetadata - Optional. If provided, preserves retryCount and firstFailedAt from a previous failure.
|
|
343
443
|
*/
|
|
344
|
-
recordTransactionFailure(entries, error, isPermanent, affectedEntityIds, affectedTables) {
|
|
444
|
+
recordTransactionFailure(entries, error, isPermanent, affectedEntityIds, affectedTables, preserveMetadata) {
|
|
345
445
|
const now = /* @__PURE__ */ new Date();
|
|
346
|
-
const
|
|
446
|
+
const normalizedEntries = this.normalizeEntries(entries);
|
|
447
|
+
const entryIds = normalizedEntries.map((e) => e.id).sort().join(",");
|
|
347
448
|
const existingIndex = this._failedTransactions.findIndex((f) => {
|
|
348
449
|
const existingIds = f.entries.map((e) => e.id).sort().join(",");
|
|
349
450
|
return existingIds === entryIds;
|
|
@@ -359,11 +460,11 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
359
460
|
};
|
|
360
461
|
} else {
|
|
361
462
|
const newFailure = {
|
|
362
|
-
id: generateFailureId(
|
|
363
|
-
entries,
|
|
463
|
+
id: generateFailureId(normalizedEntries),
|
|
464
|
+
entries: normalizedEntries,
|
|
364
465
|
error,
|
|
365
|
-
retryCount: 1,
|
|
366
|
-
firstFailedAt: now,
|
|
466
|
+
retryCount: preserveMetadata?.retryCount ?? 1,
|
|
467
|
+
firstFailedAt: preserveMetadata?.firstFailedAt ?? now,
|
|
367
468
|
lastFailedAt: now,
|
|
368
469
|
isPermanent,
|
|
369
470
|
affectedEntityIds,
|
|
@@ -375,6 +476,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
375
476
|
this._failedTransactions = this._failedTransactions.slice(-this._maxStoredFailures);
|
|
376
477
|
}
|
|
377
478
|
}
|
|
479
|
+
this._schedulePersist();
|
|
378
480
|
this._notifyFailureListeners();
|
|
379
481
|
this._notifyListeners();
|
|
380
482
|
}
|
|
@@ -385,6 +487,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
385
487
|
const initialLength = this._failedTransactions.length;
|
|
386
488
|
this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
|
|
387
489
|
if (this._failedTransactions.length !== initialLength) {
|
|
490
|
+
this._schedulePersist();
|
|
388
491
|
this._notifyFailureListeners();
|
|
389
492
|
this._notifyListeners();
|
|
390
493
|
}
|
|
@@ -395,9 +498,32 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
395
498
|
clearAllFailures() {
|
|
396
499
|
if (this._failedTransactions.length === 0) return;
|
|
397
500
|
this._failedTransactions = [];
|
|
501
|
+
this._schedulePersist();
|
|
398
502
|
this._notifyFailureListeners();
|
|
399
503
|
this._notifyListeners();
|
|
400
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Remove a failed transaction from tracking and return its entries.
|
|
507
|
+
* This is a "pop" operation - the failure is removed from the list.
|
|
508
|
+
*
|
|
509
|
+
* Note: The actual CRUD entries remain in PowerSync's ps_crud table
|
|
510
|
+
* until successfully uploaded. This just removes from our tracking.
|
|
511
|
+
*
|
|
512
|
+
* @param failureId - The failure ID to remove
|
|
513
|
+
* @returns The CrudEntry[] that were in the failure, or null if not found
|
|
514
|
+
*/
|
|
515
|
+
takeFailureForRetry(failureId) {
|
|
516
|
+
const failure = this._failedTransactions.find((f) => f.id === failureId);
|
|
517
|
+
if (!failure) {
|
|
518
|
+
this.logger.warn("[StatusTracker] Failure not found for retry:", failureId);
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
|
|
522
|
+
this._notifyFailureListeners();
|
|
523
|
+
this._schedulePersist();
|
|
524
|
+
this.logger.info("[StatusTracker] Retrieved failure for retry:", failureId, "entries:", failure.entries.length);
|
|
525
|
+
return failure.entries;
|
|
526
|
+
}
|
|
401
527
|
/**
|
|
402
528
|
* Get failures affecting a specific entity.
|
|
403
529
|
*/
|
|
@@ -442,6 +568,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
442
568
|
this._failedTransactions = this._failedTransactions.filter((f) => f.lastFailedAt.getTime() > cutoff);
|
|
443
569
|
if (this._failedTransactions.length !== initialLength) {
|
|
444
570
|
this.logger.debug(`[StatusTracker] Cleaned up ${initialLength - this._failedTransactions.length} stale failures`);
|
|
571
|
+
this._schedulePersist();
|
|
445
572
|
this._notifyFailureListeners();
|
|
446
573
|
this._notifyListeners();
|
|
447
574
|
}
|
|
@@ -452,20 +579,19 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
452
579
|
* Creates a CompletedTransaction record and adds it to history.
|
|
453
580
|
*/
|
|
454
581
|
recordTransactionComplete(entries) {
|
|
455
|
-
const
|
|
456
|
-
const
|
|
582
|
+
const normalizedEntries = this.normalizeEntries(entries);
|
|
583
|
+
const affectedTables = [...new Set(normalizedEntries.map((e) => e.table))];
|
|
584
|
+
const affectedEntityIds = [...new Set(normalizedEntries.map((e) => e.id))];
|
|
457
585
|
const id = `completed_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
458
586
|
const completed = {
|
|
459
587
|
id,
|
|
460
|
-
entries,
|
|
588
|
+
entries: normalizedEntries,
|
|
461
589
|
completedAt: /* @__PURE__ */ new Date(),
|
|
462
590
|
affectedTables,
|
|
463
591
|
affectedEntityIds
|
|
464
592
|
};
|
|
465
593
|
this._completedTransactions.unshift(completed);
|
|
466
|
-
|
|
467
|
-
this._completedTransactions = this._completedTransactions.slice(0, this._maxCompletedHistory);
|
|
468
|
-
}
|
|
594
|
+
this._schedulePersist();
|
|
469
595
|
this._notifyCompletedListeners();
|
|
470
596
|
this.logger.debug(`[StatusTracker] Recorded completed transaction: ${completed.id} (${entries.length} entries)`);
|
|
471
597
|
}
|
|
@@ -481,9 +607,22 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
481
607
|
clearCompletedHistory() {
|
|
482
608
|
if (this._completedTransactions.length === 0) return;
|
|
483
609
|
this._completedTransactions = [];
|
|
610
|
+
this._schedulePersist();
|
|
484
611
|
this._notifyCompletedListeners();
|
|
485
612
|
this.logger.debug("[StatusTracker] Cleared completed transaction history");
|
|
486
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Clear a specific completed transaction by ID.
|
|
616
|
+
*/
|
|
617
|
+
clearCompletedItem(completedId) {
|
|
618
|
+
const initialLength = this._completedTransactions.length;
|
|
619
|
+
this._completedTransactions = this._completedTransactions.filter((c) => c.id !== completedId);
|
|
620
|
+
if (this._completedTransactions.length !== initialLength) {
|
|
621
|
+
this._schedulePersist();
|
|
622
|
+
this._notifyCompletedListeners();
|
|
623
|
+
this.logger.debug("[StatusTracker] Cleared completed transaction:", completedId);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
487
626
|
/**
|
|
488
627
|
* Subscribe to completed transaction changes.
|
|
489
628
|
* @returns Unsubscribe function
|
|
@@ -495,19 +634,78 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
495
634
|
this._completedListeners.delete(listener);
|
|
496
635
|
};
|
|
497
636
|
}
|
|
637
|
+
// ─── Notification Tracking ─────────────────────────────────────────────────
|
|
638
|
+
/**
|
|
639
|
+
* Get completed transactions that occurred AFTER the last notification time.
|
|
640
|
+
* This is used for displaying "X changes synced" notifications to avoid
|
|
641
|
+
* showing stale counts from historical completed transactions.
|
|
642
|
+
*/
|
|
643
|
+
getNewCompletedTransactions() {
|
|
644
|
+
return this._completedTransactions.filter((tx) => tx.completedAt.getTime() > this._lastNotificationTime);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Mark notifications as seen by updating the last notification time.
|
|
648
|
+
* Call this when the notification is displayed or dismissed.
|
|
649
|
+
*/
|
|
650
|
+
markNotificationsAsSeen() {
|
|
651
|
+
this._lastNotificationTime = Date.now();
|
|
652
|
+
this.logger.debug("[StatusTracker] Notifications marked as seen");
|
|
653
|
+
this._notifyCompletedListeners();
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Get the timestamp of when notifications were last displayed/dismissed.
|
|
657
|
+
*/
|
|
658
|
+
getLastNotificationTime() {
|
|
659
|
+
return this._lastNotificationTime;
|
|
660
|
+
}
|
|
498
661
|
// ─── Private Methods ───────────────────────────────────────────────────────
|
|
662
|
+
/**
|
|
663
|
+
* Schedule a debounced persist operation.
|
|
664
|
+
* This prevents race conditions from multiple rapid persist calls.
|
|
665
|
+
*/
|
|
666
|
+
_schedulePersist() {
|
|
667
|
+
if (this._persistDebounceTimer) {
|
|
668
|
+
clearTimeout(this._persistDebounceTimer);
|
|
669
|
+
}
|
|
670
|
+
this._persistDebounceTimer = setTimeout(() => {
|
|
671
|
+
this._persistDebounceTimer = null;
|
|
672
|
+
this._persistTransactions();
|
|
673
|
+
}, 100);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Persist completed and failed transactions to storage.
|
|
677
|
+
*/
|
|
678
|
+
async _persistTransactions() {
|
|
679
|
+
try {
|
|
680
|
+
await Promise.all([this.storage.setItem(STORAGE_KEY_COMPLETED_TRANSACTIONS, JSON.stringify(this._completedTransactions)), this.storage.setItem(STORAGE_KEY_FAILED_TRANSACTIONS, JSON.stringify(this._failedTransactions))]);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
this.logger.warn("[StatusTracker] Failed to persist transactions:", err);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
499
685
|
_hasStatusChanged(newStatus) {
|
|
500
686
|
const old = this._state.status;
|
|
501
687
|
return old.connected !== newStatus.connected || old.connecting !== newStatus.connecting || old.hasSynced !== newStatus.hasSynced || old.uploading !== newStatus.uploading || old.downloading !== newStatus.downloading || old.lastSyncedAt?.getTime() !== newStatus.lastSyncedAt?.getTime() || old.downloadProgress?.current !== newStatus.downloadProgress?.current || old.downloadProgress?.target !== newStatus.downloadProgress?.target;
|
|
502
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Notify all listeners of status changes with throttling.
|
|
691
|
+
*
|
|
692
|
+
* Uses a "dirty" flag pattern: when throttled, we schedule a timer
|
|
693
|
+
* but get the CURRENT state when the timer fires, not the stale state
|
|
694
|
+
* from when the timer was scheduled. This ensures rapid state changes
|
|
695
|
+
* during the throttle window aren't lost.
|
|
696
|
+
*/
|
|
503
697
|
_notifyListeners(forceImmediate = false) {
|
|
504
698
|
const now = Date.now();
|
|
505
699
|
const timeSinceLastNotify = now - this._lastNotifyTime;
|
|
700
|
+
if (this._notifyTimer && !forceImmediate) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
506
703
|
if (this._notifyTimer) {
|
|
507
704
|
clearTimeout(this._notifyTimer);
|
|
508
705
|
this._notifyTimer = null;
|
|
509
706
|
}
|
|
510
707
|
const notify = () => {
|
|
708
|
+
this._notifyTimer = null;
|
|
511
709
|
this._lastNotifyTime = Date.now();
|
|
512
710
|
const status = this.getStatus();
|
|
513
711
|
this.onStatusChange?.(status);
|
|
@@ -522,8 +720,8 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
522
720
|
if (forceImmediate || timeSinceLastNotify >= this.notifyThrottleMs) {
|
|
523
721
|
notify();
|
|
524
722
|
} else {
|
|
525
|
-
const
|
|
526
|
-
this._notifyTimer = setTimeout(notify,
|
|
723
|
+
const delayMs = this.notifyThrottleMs - timeSinceLastNotify;
|
|
724
|
+
this._notifyTimer = setTimeout(notify, delayMs);
|
|
527
725
|
}
|
|
528
726
|
}
|
|
529
727
|
_notifyFailureListeners() {
|
|
@@ -546,6 +744,46 @@ var SyncStatusTracker = class _SyncStatusTracker {
|
|
|
546
744
|
}
|
|
547
745
|
}
|
|
548
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Remap a CrudEntry from persisted JSON (handles toJSON() property remapping).
|
|
749
|
+
* PowerSync's CrudEntry.toJSON() remaps: opData→data, table→type, clientId→op_id, transactionId→tx_id
|
|
750
|
+
*
|
|
751
|
+
* @returns The remapped CrudEntry, or null if critical fields (table, id) are missing
|
|
752
|
+
*/
|
|
753
|
+
remapEntry(entry) {
|
|
754
|
+
const table = entry.table ?? entry.type;
|
|
755
|
+
const id = entry.id;
|
|
756
|
+
if (!table || typeof table !== "string") {
|
|
757
|
+
this.logger.warn("[StatusTracker] Invalid CrudEntry: missing or invalid table field", entry);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
if (!id || typeof id !== "string") {
|
|
761
|
+
this.logger.warn("[StatusTracker] Invalid CrudEntry: missing or invalid id field", entry);
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
id,
|
|
766
|
+
clientId: entry.clientId ?? entry.op_id ?? 0,
|
|
767
|
+
op: entry.op,
|
|
768
|
+
table,
|
|
769
|
+
opData: entry.opData ?? entry.data,
|
|
770
|
+
transactionId: entry.transactionId ?? entry.tx_id
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Normalize CrudEntry array to plain objects to avoid CrudEntry.toJSON() remapping issues.
|
|
775
|
+
* PowerSync's CrudEntry.toJSON() remaps property names which breaks deserialization.
|
|
776
|
+
*/
|
|
777
|
+
normalizeEntries(entries) {
|
|
778
|
+
return entries.map((e) => ({
|
|
779
|
+
id: e.id,
|
|
780
|
+
clientId: e.clientId,
|
|
781
|
+
op: e.op,
|
|
782
|
+
table: e.table,
|
|
783
|
+
opData: e.opData,
|
|
784
|
+
transactionId: e.transactionId
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
549
787
|
};
|
|
550
788
|
|
|
551
789
|
// src/sync/metrics-collector.ts
|
|
@@ -779,6 +1017,7 @@ var HealthMonitor = class {
|
|
|
779
1017
|
_listeners = /* @__PURE__ */ new Set();
|
|
780
1018
|
_running = false;
|
|
781
1019
|
_paused = false;
|
|
1020
|
+
_pendingTimers = /* @__PURE__ */ new Set();
|
|
782
1021
|
constructor(logger, options = {}) {
|
|
783
1022
|
this.logger = logger;
|
|
784
1023
|
this.checkIntervalMs = options.checkIntervalMs ?? HEALTH_CHECK_INTERVAL_MS;
|
|
@@ -813,10 +1052,14 @@ var HealthMonitor = class {
|
|
|
813
1052
|
if (this._running) return;
|
|
814
1053
|
this.logger.info("[HealthMonitor] Starting");
|
|
815
1054
|
this._running = true;
|
|
816
|
-
this._checkHealth()
|
|
1055
|
+
this._checkHealth().catch((err) => {
|
|
1056
|
+
this.logger.warn("[HealthMonitor] Initial check error:", err);
|
|
1057
|
+
});
|
|
817
1058
|
this._intervalId = setInterval(() => {
|
|
818
1059
|
if (!this._paused) {
|
|
819
|
-
this._checkHealth()
|
|
1060
|
+
this._checkHealth().catch((err) => {
|
|
1061
|
+
this.logger.warn("[HealthMonitor] Periodic check error:", err);
|
|
1062
|
+
});
|
|
820
1063
|
}
|
|
821
1064
|
}, this.checkIntervalMs);
|
|
822
1065
|
}
|
|
@@ -837,6 +1080,8 @@ var HealthMonitor = class {
|
|
|
837
1080
|
*/
|
|
838
1081
|
pause() {
|
|
839
1082
|
this._paused = true;
|
|
1083
|
+
this._pendingTimers.forEach(clearTimeout);
|
|
1084
|
+
this._pendingTimers.clear();
|
|
840
1085
|
this._updateHealth({
|
|
841
1086
|
...this._health,
|
|
842
1087
|
status: "disconnected"
|
|
@@ -847,13 +1092,17 @@ var HealthMonitor = class {
|
|
|
847
1092
|
*/
|
|
848
1093
|
resume() {
|
|
849
1094
|
this._paused = false;
|
|
850
|
-
this._checkHealth()
|
|
1095
|
+
this._checkHealth().catch((err) => {
|
|
1096
|
+
this.logger.warn("[HealthMonitor] Resume check error:", err);
|
|
1097
|
+
});
|
|
851
1098
|
}
|
|
852
1099
|
/**
|
|
853
1100
|
* Dispose the monitor and clear all resources.
|
|
854
1101
|
*/
|
|
855
1102
|
dispose() {
|
|
856
1103
|
this.stop();
|
|
1104
|
+
this._pendingTimers.forEach(clearTimeout);
|
|
1105
|
+
this._pendingTimers.clear();
|
|
857
1106
|
this._listeners.clear();
|
|
858
1107
|
}
|
|
859
1108
|
// ─── Getters ───────────────────────────────────────────────────────────────
|
|
@@ -982,12 +1231,16 @@ var HealthMonitor = class {
|
|
|
982
1231
|
_withTimeout(promise, timeoutMs) {
|
|
983
1232
|
return new Promise((resolve, reject) => {
|
|
984
1233
|
const timer = setTimeout(() => {
|
|
1234
|
+
this._pendingTimers.delete(timer);
|
|
985
1235
|
reject(new Error(`Health check timeout after ${timeoutMs}ms`));
|
|
986
1236
|
}, timeoutMs);
|
|
1237
|
+
this._pendingTimers.add(timer);
|
|
987
1238
|
promise.then((result) => {
|
|
1239
|
+
this._pendingTimers.delete(timer);
|
|
988
1240
|
clearTimeout(timer);
|
|
989
1241
|
resolve(result);
|
|
990
1242
|
}, (error) => {
|
|
1243
|
+
this._pendingTimers.delete(timer);
|
|
991
1244
|
clearTimeout(timer);
|
|
992
1245
|
reject(error);
|
|
993
1246
|
});
|
|
@@ -1004,4 +1257,4 @@ export {
|
|
|
1004
1257
|
MetricsCollector,
|
|
1005
1258
|
HealthMonitor
|
|
1006
1259
|
};
|
|
1007
|
-
//# sourceMappingURL=chunk-
|
|
1260
|
+
//# sourceMappingURL=chunk-H772V6XQ.js.map
|