@pol-studios/powersync 1.0.6 → 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-R4YFWQ3Q.js → chunk-H772V6XQ.js} +304 -51
- package/dist/chunk-H772V6XQ.js.map +1 -0
- package/dist/{chunk-62J2DPKX.js → chunk-HFOFLW5F.js} +396 -412
- 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 -3
- package/dist/connector/index.js +12 -4
- 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 +100 -12
- package/dist/index.js +148 -38
- package/dist/index.native.d.ts +20 -10
- package/dist/index.native.js +148 -39
- package/dist/index.web.d.ts +20 -10
- package/dist/index.web.js +149 -39
- 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 +447 -21
- package/dist/provider/index.js +33 -13
- 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-l3iL9Jte.d.ts → supabase-connector-qLm-WHkM.d.ts} +90 -25
- package/dist/sync/index.d.ts +288 -23
- package/dist/sync/index.js +22 -10
- 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-5FIMA26D.js +0 -1
- package/dist/chunk-62J2DPKX.js.map +0 -1
- package/dist/chunk-7EMDVIZX.js.map +0 -1
- package/dist/chunk-EJ23MXPQ.js.map +0 -1
- package/dist/chunk-FPTDATY5.js.map +0 -1
- package/dist/chunk-KCDG2MNP.js +0 -1431
- package/dist/chunk-KCDG2MNP.js.map +0 -1
- package/dist/chunk-OLHGI472.js +0 -1
- package/dist/chunk-PAFBKNL3.js +0 -99
- package/dist/chunk-PAFBKNL3.js.map +0 -1
- package/dist/chunk-R4YFWQ3Q.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/failed-upload-store-C0cLxxPz.d.ts +0 -33
- /package/dist/{chunk-32OLICZO.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-5FIMA26D.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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
withExponentialBackoff
|
|
3
|
+
} from "./chunk-FV2HXEIY.js";
|
|
4
4
|
import {
|
|
5
5
|
classifySupabaseError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XQAJM2MW.js";
|
|
7
7
|
|
|
8
8
|
// src/connector/types.ts
|
|
9
9
|
var defaultSchemaRouter = () => "public";
|
|
@@ -11,18 +11,60 @@ var DEFAULT_RETRY_CONFIG = {
|
|
|
11
11
|
transient: {
|
|
12
12
|
maxRetries: 3,
|
|
13
13
|
baseDelayMs: 1e3,
|
|
14
|
-
|
|
14
|
+
// 1 second initial delay
|
|
15
|
+
maxDelayMs: 4e3,
|
|
16
|
+
// 4 second cap
|
|
15
17
|
backoffMultiplier: 2
|
|
18
|
+
// 1s → 2s → 4s
|
|
16
19
|
},
|
|
17
20
|
permanent: {
|
|
18
|
-
maxRetries:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
maxRetries: 0,
|
|
22
|
+
// No retries - permanent errors won't succeed
|
|
23
|
+
baseDelayMs: 1e3,
|
|
24
|
+
// Irrelevant with 0 retries, but needed for type
|
|
25
|
+
maxDelayMs: 1e3,
|
|
26
|
+
// Irrelevant with 0 retries
|
|
27
|
+
backoffMultiplier: 1
|
|
28
|
+
// Irrelevant with 0 retries
|
|
22
29
|
}
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
// src/conflicts/detect.ts
|
|
33
|
+
var ConflictDetectionError = class extends Error {
|
|
34
|
+
constructor(message, options) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ConflictDetectionError";
|
|
37
|
+
this.cause = options?.cause;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function deepEqual(a, b, maxDepth = 10) {
|
|
41
|
+
if (a === b) return true;
|
|
42
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
43
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
44
|
+
}
|
|
45
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
46
|
+
if (typeof a !== typeof b) return false;
|
|
47
|
+
if (typeof a !== "object") return false;
|
|
48
|
+
if (maxDepth <= 0) {
|
|
49
|
+
console.warn("[deepEqual] Max depth reached, falling back to reference equality");
|
|
50
|
+
return a === b;
|
|
51
|
+
}
|
|
52
|
+
if (a instanceof Date && b instanceof Date) {
|
|
53
|
+
return a.getTime() === b.getTime();
|
|
54
|
+
}
|
|
55
|
+
if (a instanceof Date || b instanceof Date) return false;
|
|
56
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
57
|
+
const keysA = Object.keys(a);
|
|
58
|
+
const keysB = Object.keys(b);
|
|
59
|
+
if (keysA.length !== keysB.length) return false;
|
|
60
|
+
for (const key of keysA) {
|
|
61
|
+
if (!keysB.includes(key)) return false;
|
|
62
|
+
if (!deepEqual(a[key], b[key], maxDepth - 1)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
26
68
|
var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
27
69
|
function validateTableName(table) {
|
|
28
70
|
if (!TABLE_NAME_REGEX.test(table)) {
|
|
@@ -54,14 +96,9 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
|
|
|
54
96
|
ascending: false
|
|
55
97
|
}).limit(20);
|
|
56
98
|
if (error) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
conflicts: [],
|
|
61
|
-
nonConflictingChanges: Object.keys(filteredPendingChanges),
|
|
62
|
-
table,
|
|
63
|
-
recordId
|
|
64
|
-
};
|
|
99
|
+
throw new ConflictDetectionError(`AuditLog query failed for ${table}/${recordId}`, {
|
|
100
|
+
cause: error
|
|
101
|
+
});
|
|
65
102
|
}
|
|
66
103
|
const serverChanges = /* @__PURE__ */ new Map();
|
|
67
104
|
for (const log of auditLogs ?? []) {
|
|
@@ -70,7 +107,7 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
|
|
|
70
107
|
if (!oldRec || !newRec) continue;
|
|
71
108
|
for (const [field, newValue] of Object.entries(newRec)) {
|
|
72
109
|
if (ignoredFields.has(field)) continue;
|
|
73
|
-
if (oldRec[field]
|
|
110
|
+
if (!deepEqual(oldRec[field], newValue) && !serverChanges.has(field)) {
|
|
74
111
|
serverChanges.set(field, {
|
|
75
112
|
newValue,
|
|
76
113
|
changedBy: log.changeBy,
|
|
@@ -129,121 +166,63 @@ async function getLocalVersion(table, recordId, db) {
|
|
|
129
166
|
return result?._version ?? null;
|
|
130
167
|
}
|
|
131
168
|
|
|
132
|
-
// src/
|
|
133
|
-
var
|
|
134
|
-
constructor(message
|
|
169
|
+
// src/connector/supabase-connector.ts
|
|
170
|
+
var ValidationError = class extends Error {
|
|
171
|
+
constructor(message) {
|
|
135
172
|
super(message);
|
|
136
|
-
this.name = "
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
var RetryExhaustedError = class extends Error {
|
|
140
|
-
/** The last error that caused the final retry to fail */
|
|
141
|
-
cause;
|
|
142
|
-
/** Total number of attempts made */
|
|
143
|
-
attempts;
|
|
144
|
-
constructor(cause, attempts) {
|
|
145
|
-
super(`Retry exhausted after ${attempts} attempt(s): ${cause.message}`);
|
|
146
|
-
this.name = "RetryExhaustedError";
|
|
147
|
-
this.cause = cause;
|
|
148
|
-
this.attempts = attempts;
|
|
173
|
+
this.name = "ValidationError";
|
|
149
174
|
}
|
|
150
175
|
};
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const safeAttempt = Math.max(0, attempt);
|
|
158
|
-
const exponentialDelay = baseDelayMs * Math.pow(backoffMultiplier, safeAttempt);
|
|
159
|
-
return Math.min(exponentialDelay, maxDelayMs);
|
|
160
|
-
}
|
|
161
|
-
function addJitter(delay) {
|
|
162
|
-
const jitterFactor = 0.9 + Math.random() * 0.2;
|
|
163
|
-
return Math.round(delay * jitterFactor);
|
|
164
|
-
}
|
|
165
|
-
function sleep(ms, signal) {
|
|
166
|
-
return new Promise((resolve, reject) => {
|
|
167
|
-
if (signal?.aborted) {
|
|
168
|
-
reject(new AbortError());
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (ms <= 0) {
|
|
172
|
-
resolve();
|
|
173
|
-
return;
|
|
176
|
+
function isAuthError(error) {
|
|
177
|
+
if (!error) return false;
|
|
178
|
+
if (typeof error === "object" && error !== null) {
|
|
179
|
+
const statusCode = error.status ?? error.statusCode;
|
|
180
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
181
|
+
return true;
|
|
174
182
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
clearTimeout(timeoutId);
|
|
179
|
-
}
|
|
180
|
-
reject(new AbortError());
|
|
181
|
-
};
|
|
182
|
-
if (signal) {
|
|
183
|
-
signal.addEventListener("abort", handleAbort, {
|
|
184
|
-
once: true
|
|
185
|
-
});
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code === "401" || code === "403" || code === "42501") {
|
|
185
|
+
return true;
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
}
|
|
188
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
189
|
+
const authPatterns = [
|
|
190
|
+
/\bjwt\s+(expired|invalid|malformed)\b/,
|
|
191
|
+
/\btoken\s+(expired|invalid|revoked)\b/,
|
|
192
|
+
/\bsession\s+(expired|invalid)\b/,
|
|
193
|
+
/\bunauthorized\s*(access|request|error)?\b/,
|
|
194
|
+
/\baccess\s+(denied|forbidden)\b/,
|
|
195
|
+
/\bnot\s+authorized\b/,
|
|
196
|
+
/\bauth(entication)?\s+(failed|error|required)\b/,
|
|
197
|
+
/\bpermission\s+denied\b/,
|
|
198
|
+
/\binvalid\s+(credentials|api[_\s]?key)\b/,
|
|
199
|
+
/\brls\b.*\bpolicy\b/,
|
|
200
|
+
/\brow[_\s]?level[_\s]?security\b/,
|
|
201
|
+
/\b42501\b/
|
|
202
|
+
// PostgreSQL insufficient privilege error code
|
|
203
|
+
];
|
|
204
|
+
return authPatterns.some((pattern) => pattern.test(message));
|
|
205
|
+
}
|
|
206
|
+
function withTimeout(promise, ms, message) {
|
|
207
|
+
let timeoutId;
|
|
208
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
209
|
+
timeoutId = setTimeout(() => reject(new Error(message)), ms);
|
|
193
210
|
});
|
|
211
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
194
212
|
}
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
baseDelayMs,
|
|
205
|
-
maxDelayMs,
|
|
206
|
-
backoffMultiplier
|
|
207
|
-
} = config;
|
|
208
|
-
const {
|
|
209
|
-
signal,
|
|
210
|
-
onRetry
|
|
211
|
-
} = options ?? {};
|
|
212
|
-
if (signal?.aborted) {
|
|
213
|
-
throw new AbortError();
|
|
214
|
-
}
|
|
215
|
-
const safeMaxRetries = Math.max(0, Math.floor(maxRetries));
|
|
216
|
-
const totalAttempts = safeMaxRetries + 1;
|
|
217
|
-
let lastError;
|
|
218
|
-
for (let attempt = 0; attempt < totalAttempts; attempt++) {
|
|
219
|
-
if (signal?.aborted) {
|
|
220
|
-
throw new AbortError();
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
return await fn();
|
|
224
|
-
} catch (error) {
|
|
225
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
226
|
-
const isLastAttempt = attempt === totalAttempts - 1;
|
|
227
|
-
if (isLastAttempt) {
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
if (signal?.aborted) {
|
|
231
|
-
throw new AbortError();
|
|
232
|
-
}
|
|
233
|
-
const baseDelay = calculateBackoffDelay(attempt, {
|
|
234
|
-
baseDelayMs,
|
|
235
|
-
maxDelayMs,
|
|
236
|
-
backoffMultiplier
|
|
237
|
-
});
|
|
238
|
-
const delayWithJitter = addJitter(baseDelay);
|
|
239
|
-
onRetry?.(attempt + 1, delayWithJitter, lastError);
|
|
240
|
-
await sleep(delayWithJitter, signal);
|
|
213
|
+
var MAX_PAYLOAD_SIZE = 900 * 1024;
|
|
214
|
+
function groupEntriesByTable(entries) {
|
|
215
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const existing = grouped.get(entry.table);
|
|
218
|
+
if (existing) {
|
|
219
|
+
existing.push(entry);
|
|
220
|
+
} else {
|
|
221
|
+
grouped.set(entry.table, [entry]);
|
|
241
222
|
}
|
|
242
223
|
}
|
|
243
|
-
|
|
224
|
+
return grouped;
|
|
244
225
|
}
|
|
245
|
-
|
|
246
|
-
// src/connector/supabase-connector.ts
|
|
247
226
|
var SupabaseConnector = class {
|
|
248
227
|
supabase;
|
|
249
228
|
powerSyncUrl;
|
|
@@ -267,6 +246,10 @@ var SupabaseConnector = class {
|
|
|
267
246
|
resolvedConflicts = /* @__PURE__ */ new Map();
|
|
268
247
|
// Cleanup function for resolution listener subscription
|
|
269
248
|
unsubscribeResolution;
|
|
249
|
+
// Promise-based locking for version column checks to prevent duplicate queries
|
|
250
|
+
versionColumnPromises = /* @__PURE__ */ new Map();
|
|
251
|
+
// Flag to track if connector has been destroyed
|
|
252
|
+
isDestroyed = false;
|
|
270
253
|
// Retry configuration
|
|
271
254
|
retryConfig;
|
|
272
255
|
autoRetryPaused = false;
|
|
@@ -313,11 +296,13 @@ var SupabaseConnector = class {
|
|
|
313
296
|
* Call this when the connector is no longer needed.
|
|
314
297
|
*/
|
|
315
298
|
destroy() {
|
|
299
|
+
this.isDestroyed = true;
|
|
316
300
|
if (this.unsubscribeResolution) {
|
|
317
301
|
this.unsubscribeResolution();
|
|
318
302
|
this.unsubscribeResolution = void 0;
|
|
319
303
|
}
|
|
320
304
|
this.resolvedConflicts.clear();
|
|
305
|
+
this.versionColumnPromises.clear();
|
|
321
306
|
}
|
|
322
307
|
// ─── Retry Control Methods ─────────────────────────────────────────────────
|
|
323
308
|
/**
|
|
@@ -339,113 +324,81 @@ var SupabaseConnector = class {
|
|
|
339
324
|
console.log("[Connector] Auto-retry resumed");
|
|
340
325
|
}
|
|
341
326
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Manually retry all failed uploads that are ready for retry.
|
|
344
|
-
* This processes entries from the failed upload store.
|
|
345
|
-
*/
|
|
346
|
-
async retryFailedUploads() {
|
|
347
|
-
const retryableUploads = failedUploadStore.getRetryable();
|
|
348
|
-
if (retryableUploads.length === 0) {
|
|
349
|
-
if (__DEV__) {
|
|
350
|
-
console.log("[Connector] No failed uploads ready for retry");
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (__DEV__) {
|
|
355
|
-
console.log("[Connector] Manually retrying failed uploads:", {
|
|
356
|
-
count: retryableUploads.length,
|
|
357
|
-
entries: retryableUploads.map((u) => ({
|
|
358
|
-
table: u.table,
|
|
359
|
-
id: u.id,
|
|
360
|
-
operation: u.operation
|
|
361
|
-
}))
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
for (const upload of retryableUploads) {
|
|
365
|
-
try {
|
|
366
|
-
const entry = {
|
|
367
|
-
table: upload.table,
|
|
368
|
-
op: upload.operation,
|
|
369
|
-
id: upload.id,
|
|
370
|
-
clientId: Date.now(),
|
|
371
|
-
// Synthetic clientId for retry
|
|
372
|
-
opData: upload.data
|
|
373
|
-
};
|
|
374
|
-
await this.processWithRetry(entry);
|
|
375
|
-
failedUploadStore.remove(upload.id);
|
|
376
|
-
if (__DEV__) {
|
|
377
|
-
console.log("[Connector] Retry succeeded for:", {
|
|
378
|
-
table: upload.table,
|
|
379
|
-
id: upload.id
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
} catch (error) {
|
|
383
|
-
if (__DEV__) {
|
|
384
|
-
console.log("[Connector] Retry failed again for:", {
|
|
385
|
-
table: upload.table,
|
|
386
|
-
id: upload.id,
|
|
387
|
-
error: error instanceof Error ? error.message : String(error)
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Clear all failed uploads from the store.
|
|
395
|
-
* Use with caution - this discards all pending retries.
|
|
396
|
-
*/
|
|
397
|
-
clearFailedUploads() {
|
|
398
|
-
failedUploadStore.clear();
|
|
399
|
-
if (__DEV__) {
|
|
400
|
-
console.log("[Connector] Failed uploads cleared");
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Get all failed uploads from the store.
|
|
405
|
-
*/
|
|
406
|
-
getFailedUploads() {
|
|
407
|
-
return failedUploadStore.getAll();
|
|
408
|
-
}
|
|
409
327
|
// ─── Private Retry Logic ───────────────────────────────────────────────────
|
|
410
328
|
/**
|
|
411
329
|
* Process a single CRUD entry with exponential backoff retry.
|
|
412
330
|
*
|
|
331
|
+
* This method uses a two-phase approach:
|
|
332
|
+
* 1. First attempt - try the operation to classify the error type
|
|
333
|
+
* 2. Retry phase - use the appropriate config (transient vs permanent) based on classification
|
|
334
|
+
*
|
|
335
|
+
* This fixes the issue where reassigning selectedConfig inside withExponentialBackoff's
|
|
336
|
+
* callback had no effect because the config was already destructured at call time.
|
|
337
|
+
*
|
|
413
338
|
* @param entry - The CRUD entry to process
|
|
414
339
|
* @throws Error if all retries exhausted (for critical failures)
|
|
415
340
|
*/
|
|
416
341
|
async processWithRetry(entry) {
|
|
342
|
+
if (this.isDestroyed) {
|
|
343
|
+
throw new Error("Connector destroyed");
|
|
344
|
+
}
|
|
417
345
|
const classified = {
|
|
418
346
|
isPermanent: false,
|
|
419
347
|
pgCode: void 0,
|
|
420
348
|
userMessage: ""
|
|
421
349
|
};
|
|
422
|
-
let selectedConfig = this.retryConfig.transient;
|
|
423
350
|
let lastError;
|
|
424
351
|
try {
|
|
425
|
-
await
|
|
352
|
+
await this.processCrudEntry(entry);
|
|
353
|
+
return;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
356
|
+
const classifiedError = classifySupabaseError(error);
|
|
357
|
+
classified.isPermanent = classifiedError.isPermanent;
|
|
358
|
+
classified.pgCode = classifiedError.pgCode;
|
|
359
|
+
classified.userMessage = classifiedError.userMessage;
|
|
360
|
+
if (isAuthError(error)) {
|
|
361
|
+
if (__DEV__) {
|
|
362
|
+
console.log("[Connector] Auth error detected, refreshing session before retry:", {
|
|
363
|
+
table: entry.table,
|
|
364
|
+
op: entry.op,
|
|
365
|
+
id: entry.id
|
|
366
|
+
});
|
|
367
|
+
}
|
|
426
368
|
try {
|
|
369
|
+
await this.supabase.auth.refreshSession();
|
|
427
370
|
await this.processCrudEntry(entry);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
classified.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
437
|
-
if (__DEV__) {
|
|
438
|
-
console.log("[Connector] CRUD operation failed, will retry:", {
|
|
439
|
-
table: entry.table,
|
|
440
|
-
op: entry.op,
|
|
441
|
-
id: entry.id,
|
|
442
|
-
isPermanent: classifiedError.isPermanent,
|
|
443
|
-
pgCode: classifiedError.pgCode,
|
|
444
|
-
userMessage: classifiedError.userMessage
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
throw error;
|
|
371
|
+
return;
|
|
372
|
+
} catch (retryError) {
|
|
373
|
+
lastError = retryError instanceof Error ? retryError : new Error(String(retryError));
|
|
374
|
+
const retriedClassification = classifySupabaseError(retryError);
|
|
375
|
+
classified.isPermanent = retriedClassification.isPermanent;
|
|
376
|
+
classified.pgCode = retriedClassification.pgCode;
|
|
377
|
+
classified.userMessage = retriedClassification.userMessage;
|
|
448
378
|
}
|
|
379
|
+
}
|
|
380
|
+
if (__DEV__) {
|
|
381
|
+
console.log("[Connector] Initial attempt failed, will retry with appropriate config:", {
|
|
382
|
+
table: entry.table,
|
|
383
|
+
op: entry.op,
|
|
384
|
+
id: entry.id,
|
|
385
|
+
isPermanent: classified.isPermanent,
|
|
386
|
+
pgCode: classified.pgCode,
|
|
387
|
+
userMessage: classified.userMessage
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
this.logger?.warn("[Connector] Initial attempt failed:", {
|
|
391
|
+
table: entry.table,
|
|
392
|
+
op: entry.op,
|
|
393
|
+
id: entry.id,
|
|
394
|
+
error: lastError.message,
|
|
395
|
+
isPermanent: classified.isPermanent
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const selectedConfig = classified.isPermanent ? this.retryConfig.permanent : this.retryConfig.transient;
|
|
399
|
+
try {
|
|
400
|
+
await withExponentialBackoff(async () => {
|
|
401
|
+
await this.processCrudEntry(entry);
|
|
449
402
|
}, selectedConfig, {
|
|
450
403
|
onRetry: (attempt, delay, error) => {
|
|
451
404
|
this.logger?.debug("[Connector] Retry attempt:", {
|
|
@@ -462,6 +415,7 @@ var SupabaseConnector = class {
|
|
|
462
415
|
op: entry.op,
|
|
463
416
|
id: entry.id,
|
|
464
417
|
attempt,
|
|
418
|
+
maxRetries: selectedConfig.maxRetries,
|
|
465
419
|
delayMs: delay,
|
|
466
420
|
errorMessage: error.message
|
|
467
421
|
});
|
|
@@ -469,31 +423,15 @@ var SupabaseConnector = class {
|
|
|
469
423
|
}
|
|
470
424
|
});
|
|
471
425
|
} catch (error) {
|
|
472
|
-
const finalError =
|
|
426
|
+
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
473
427
|
const category = classified.isPermanent ? "permanent" : classified.pgCode ? "transient" : "unknown";
|
|
474
|
-
const retryConfig = classified.isPermanent ? this.retryConfig.permanent : this.retryConfig.transient;
|
|
475
|
-
const nextRetryDelay = calculateBackoffDelay(retryConfig.maxRetries, retryConfig);
|
|
476
|
-
const nextRetryAt = Date.now() + nextRetryDelay;
|
|
477
|
-
failedUploadStore.add({
|
|
478
|
-
table: entry.table,
|
|
479
|
-
operation: entry.op,
|
|
480
|
-
data: entry.opData ?? {},
|
|
481
|
-
error: {
|
|
482
|
-
message: finalError.message,
|
|
483
|
-
code: classified.pgCode,
|
|
484
|
-
category
|
|
485
|
-
},
|
|
486
|
-
retryCount: retryConfig.maxRetries,
|
|
487
|
-
lastAttempt: Date.now(),
|
|
488
|
-
nextRetryAt
|
|
489
|
-
});
|
|
490
428
|
if (__DEV__) {
|
|
491
|
-
console.log("[Connector]
|
|
429
|
+
console.log("[Connector] CRUD entry failed after retries, leaving in ps_crud:", {
|
|
492
430
|
table: entry.table,
|
|
493
431
|
op: entry.op,
|
|
494
432
|
id: entry.id,
|
|
495
433
|
category,
|
|
496
|
-
|
|
434
|
+
error: finalError.message
|
|
497
435
|
});
|
|
498
436
|
}
|
|
499
437
|
this.logger?.error("[Connector] CRUD entry failed after retries:", {
|
|
@@ -501,12 +439,9 @@ var SupabaseConnector = class {
|
|
|
501
439
|
op: entry.op,
|
|
502
440
|
id: entry.id,
|
|
503
441
|
error: finalError.message,
|
|
504
|
-
category
|
|
505
|
-
nextRetryAt
|
|
442
|
+
category
|
|
506
443
|
});
|
|
507
|
-
|
|
508
|
-
throw finalError;
|
|
509
|
-
}
|
|
444
|
+
throw finalError;
|
|
510
445
|
}
|
|
511
446
|
}
|
|
512
447
|
/**
|
|
@@ -564,58 +499,33 @@ var SupabaseConnector = class {
|
|
|
564
499
|
* 5. Applies resolution or skips entry based on handler response
|
|
565
500
|
*/
|
|
566
501
|
async uploadData(database) {
|
|
502
|
+
if (this.isDestroyed) {
|
|
503
|
+
throw new Error("Connector destroyed - aborting upload");
|
|
504
|
+
}
|
|
567
505
|
if (this.shouldUploadFn && !this.shouldUploadFn()) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
this.logger?.debug("[Connector] Upload skipped - sync mode does not allow uploads");
|
|
572
|
-
return;
|
|
506
|
+
this.logger?.debug("[Connector] Upload blocked - not currently permitted, will retry");
|
|
507
|
+
throw new Error("Upload not permitted - will retry on next sync cycle");
|
|
573
508
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const entry = {
|
|
585
|
-
table: upload.table,
|
|
586
|
-
op: upload.operation,
|
|
587
|
-
id: upload.id,
|
|
588
|
-
clientId: Date.now(),
|
|
589
|
-
// Synthetic clientId for retry
|
|
590
|
-
opData: upload.data
|
|
591
|
-
};
|
|
592
|
-
await this.processWithRetry(entry);
|
|
593
|
-
failedUploadStore.remove(upload.id);
|
|
594
|
-
if (__DEV__) {
|
|
595
|
-
console.log("[Connector] Retried upload succeeded:", {
|
|
596
|
-
table: upload.table,
|
|
597
|
-
id: upload.id
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
} catch (error) {
|
|
601
|
-
if (__DEV__) {
|
|
602
|
-
console.log("[Connector] Retried upload failed again:", {
|
|
603
|
-
table: upload.table,
|
|
604
|
-
id: upload.id,
|
|
605
|
-
error: error instanceof Error ? error.message : String(error)
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
509
|
+
const {
|
|
510
|
+
data: {
|
|
511
|
+
session
|
|
512
|
+
},
|
|
513
|
+
error: sessionError
|
|
514
|
+
} = await this.supabase.auth.getSession();
|
|
515
|
+
if (sessionError || !session) {
|
|
516
|
+
const noSessionError = new Error("No active session - cannot upload data");
|
|
517
|
+
if (__DEV__) {
|
|
518
|
+
console.error("[Connector] uploadData failed: no session");
|
|
610
519
|
}
|
|
520
|
+
throw noSessionError;
|
|
611
521
|
}
|
|
612
522
|
if (__DEV__) {
|
|
613
523
|
console.log("[Connector] uploadData called, fetching next CRUD transaction...");
|
|
614
524
|
}
|
|
615
525
|
const transaction = await database.getNextCrudTransaction();
|
|
616
|
-
if (!transaction) {
|
|
526
|
+
if (!transaction || transaction.crud.length === 0) {
|
|
617
527
|
if (__DEV__) {
|
|
618
|
-
console.log("[Connector] No pending CRUD transaction found");
|
|
528
|
+
console.log("[Connector] No pending CRUD transaction found or transaction is empty");
|
|
619
529
|
}
|
|
620
530
|
return;
|
|
621
531
|
}
|
|
@@ -672,6 +582,9 @@ var SupabaseConnector = class {
|
|
|
672
582
|
entriesDiscarded.push(entry);
|
|
673
583
|
break;
|
|
674
584
|
case "partial":
|
|
585
|
+
if (!existingResolution.fields || existingResolution.fields.length === 0) {
|
|
586
|
+
throw new Error("Partial resolution requires at least one field");
|
|
587
|
+
}
|
|
675
588
|
const partialEntry = {
|
|
676
589
|
...entry,
|
|
677
590
|
opData: this.filterFields(entry.opData ?? {}, existingResolution.fields)
|
|
@@ -726,6 +639,9 @@ var SupabaseConnector = class {
|
|
|
726
639
|
}
|
|
727
640
|
break;
|
|
728
641
|
case "partial":
|
|
642
|
+
if (!resolution.fields || resolution.fields.length === 0) {
|
|
643
|
+
throw new Error("Partial resolution requires at least one field");
|
|
644
|
+
}
|
|
729
645
|
const partialEntry = {
|
|
730
646
|
...entry,
|
|
731
647
|
opData: this.filterFields(entry.opData ?? {}, resolution.fields)
|
|
@@ -738,7 +654,7 @@ var SupabaseConnector = class {
|
|
|
738
654
|
break;
|
|
739
655
|
}
|
|
740
656
|
} else {
|
|
741
|
-
|
|
657
|
+
this.logger?.warn("[Connector] Conflict detected but no handler:", {
|
|
742
658
|
table: entry.table,
|
|
743
659
|
id: entry.id,
|
|
744
660
|
conflicts: conflictResult.conflicts
|
|
@@ -755,7 +671,7 @@ var SupabaseConnector = class {
|
|
|
755
671
|
});
|
|
756
672
|
}
|
|
757
673
|
this.onTransactionComplete?.(entriesQueuedForUI);
|
|
758
|
-
|
|
674
|
+
throw new Error("Entries queued for UI resolution - retry will occur on next sync cycle");
|
|
759
675
|
}
|
|
760
676
|
if (entriesToProcess.length === 0 && entriesDiscarded.length > 0) {
|
|
761
677
|
if (__DEV__) {
|
|
@@ -772,7 +688,6 @@ var SupabaseConnector = class {
|
|
|
772
688
|
}
|
|
773
689
|
return;
|
|
774
690
|
}
|
|
775
|
-
const criticalFailures = [];
|
|
776
691
|
const successfulEntries = [];
|
|
777
692
|
for (const entry of entriesToProcess) {
|
|
778
693
|
if (__DEV__) {
|
|
@@ -783,54 +698,52 @@ var SupabaseConnector = class {
|
|
|
783
698
|
opData: entry.opData
|
|
784
699
|
});
|
|
785
700
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
successfulEntries.push(entry);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
criticalFailures.push({
|
|
791
|
-
entry,
|
|
792
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (criticalFailures.length > 0) {
|
|
797
|
-
const firstFailure = criticalFailures[0];
|
|
798
|
-
const classified = classifySupabaseError(firstFailure.error);
|
|
799
|
-
console.error("[PowerSync Connector] Critical upload failure:", {
|
|
800
|
-
errorMessage: firstFailure.error.message,
|
|
801
|
-
classified,
|
|
802
|
-
isPermanent: classified.isPermanent,
|
|
803
|
-
criticalCount: criticalFailures.length,
|
|
804
|
-
successCount: successfulEntries.length,
|
|
805
|
-
entries: criticalFailures.map((f) => ({
|
|
806
|
-
table: f.entry.table,
|
|
807
|
-
op: f.entry.op,
|
|
808
|
-
id: f.entry.id
|
|
809
|
-
}))
|
|
810
|
-
});
|
|
811
|
-
this.logger?.error("[Connector] Critical upload failure:", {
|
|
812
|
-
error: firstFailure.error,
|
|
813
|
-
classified,
|
|
814
|
-
entries: criticalFailures.map((f) => ({
|
|
815
|
-
table: f.entry.table,
|
|
816
|
-
op: f.entry.op,
|
|
817
|
-
id: f.entry.id
|
|
818
|
-
}))
|
|
819
|
-
});
|
|
820
|
-
this.onTransactionFailure?.(criticalFailures.map((f) => f.entry), firstFailure.error, classified);
|
|
821
|
-
throw firstFailure.error;
|
|
701
|
+
await this.processWithRetry(entry);
|
|
702
|
+
successfulEntries.push(entry);
|
|
822
703
|
}
|
|
704
|
+
await this.finalizeTransaction({
|
|
705
|
+
transaction,
|
|
706
|
+
successfulEntries,
|
|
707
|
+
discardedEntries: entriesDiscarded,
|
|
708
|
+
partialResolutions
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Finalize a transaction by completing it after all entries processed successfully.
|
|
713
|
+
* Extracted to eliminate duplication between uploadData and processTransaction.
|
|
714
|
+
*
|
|
715
|
+
* @param context - The finalization context containing results and transaction
|
|
716
|
+
*/
|
|
717
|
+
async finalizeTransaction(context) {
|
|
718
|
+
const {
|
|
719
|
+
transaction,
|
|
720
|
+
successfulEntries,
|
|
721
|
+
discardedEntries = [],
|
|
722
|
+
partialResolutions = []
|
|
723
|
+
} = context;
|
|
823
724
|
if (__DEV__) {
|
|
824
725
|
console.log("[Connector] All CRUD entries processed, completing transaction...");
|
|
825
726
|
}
|
|
826
727
|
try {
|
|
827
|
-
await transaction.complete();
|
|
728
|
+
await withTimeout(transaction.complete(), 3e4, "Transaction complete timeout");
|
|
828
729
|
if (__DEV__) {
|
|
829
730
|
console.log("[Connector] Transaction completed successfully:", {
|
|
830
731
|
entriesCount: successfulEntries.length,
|
|
831
|
-
discardedCount:
|
|
732
|
+
discardedCount: discardedEntries.length
|
|
832
733
|
});
|
|
833
734
|
}
|
|
735
|
+
for (const entry of successfulEntries) {
|
|
736
|
+
this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);
|
|
737
|
+
}
|
|
738
|
+
for (const entry of discardedEntries) {
|
|
739
|
+
this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);
|
|
740
|
+
}
|
|
741
|
+
if (this.resolvedConflicts.size > 100) {
|
|
742
|
+
const oldest = [...this.resolvedConflicts.keys()][0];
|
|
743
|
+
if (oldest) {
|
|
744
|
+
this.resolvedConflicts.delete(oldest);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
834
747
|
if (this.conflictBus && partialResolutions.length > 0) {
|
|
835
748
|
for (const {
|
|
836
749
|
originalConflict,
|
|
@@ -861,10 +774,8 @@ var SupabaseConnector = class {
|
|
|
861
774
|
this.onTransactionComplete?.(successfulEntries);
|
|
862
775
|
} catch (error) {
|
|
863
776
|
const classified = classifySupabaseError(error);
|
|
864
|
-
|
|
777
|
+
this.logger?.error("[Connector] Transaction completion FAILED:", {
|
|
865
778
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
866
|
-
errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
|
|
867
|
-
errorObject: JSON.stringify(error, null, 2),
|
|
868
779
|
classified,
|
|
869
780
|
isPermanent: classified.isPermanent,
|
|
870
781
|
entries: successfulEntries.map((e) => ({
|
|
@@ -873,6 +784,12 @@ var SupabaseConnector = class {
|
|
|
873
784
|
id: e.id
|
|
874
785
|
}))
|
|
875
786
|
});
|
|
787
|
+
if (__DEV__) {
|
|
788
|
+
console.error("[Connector] Transaction completion error details:", {
|
|
789
|
+
errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
|
|
790
|
+
errorObject: JSON.stringify(error, null, 2)
|
|
791
|
+
});
|
|
792
|
+
}
|
|
876
793
|
this.logger?.error("[Connector] Transaction completion error:", {
|
|
877
794
|
error,
|
|
878
795
|
classified,
|
|
@@ -888,106 +805,175 @@ var SupabaseConnector = class {
|
|
|
888
805
|
}
|
|
889
806
|
/**
|
|
890
807
|
* Process a transaction without conflict detection.
|
|
891
|
-
*
|
|
808
|
+
* Uses batched operations for PUT and DELETE, individual processing for PATCH.
|
|
892
809
|
*/
|
|
893
810
|
async processTransaction(transaction, _database) {
|
|
894
|
-
const criticalFailures = [];
|
|
895
811
|
const successfulEntries = [];
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
812
|
+
const entriesByTable = groupEntriesByTable(transaction.crud);
|
|
813
|
+
if (__DEV__) {
|
|
814
|
+
console.log("[Connector] Processing transaction with batching:", {
|
|
815
|
+
totalEntries: transaction.crud.length,
|
|
816
|
+
tables: Array.from(entriesByTable.keys()),
|
|
817
|
+
entriesPerTable: Object.fromEntries(Array.from(entriesByTable.entries()).map(([table, entries]) => [table, {
|
|
818
|
+
total: entries.length,
|
|
819
|
+
put: entries.filter((e) => e.op === "PUT" /* PUT */).length,
|
|
820
|
+
patch: entries.filter((e) => e.op === "PATCH" /* PATCH */).length,
|
|
821
|
+
delete: entries.filter((e) => e.op === "DELETE" /* DELETE */).length
|
|
822
|
+
}]))
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
for (const [table, entries] of entriesByTable) {
|
|
826
|
+
const schema = this.schemaRouter(table);
|
|
827
|
+
const putEntries = entries.filter((e) => e.op === "PUT" /* PUT */);
|
|
828
|
+
const patchEntries = entries.filter((e) => e.op === "PATCH" /* PATCH */);
|
|
829
|
+
const deleteEntries = entries.filter((e) => e.op === "DELETE" /* DELETE */);
|
|
830
|
+
if (this.crudHandler) {
|
|
831
|
+
for (const entry of entries) {
|
|
832
|
+
await this.processWithRetry(entry);
|
|
833
|
+
successfulEntries.push(entry);
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
904
836
|
}
|
|
905
|
-
|
|
837
|
+
if (putEntries.length > 0) {
|
|
838
|
+
const successful = await this.processBatchedPuts(table, schema, putEntries);
|
|
839
|
+
successfulEntries.push(...successful);
|
|
840
|
+
}
|
|
841
|
+
if (deleteEntries.length > 0) {
|
|
842
|
+
const successful = await this.processBatchedDeletes(table, schema, deleteEntries);
|
|
843
|
+
successfulEntries.push(...successful);
|
|
844
|
+
}
|
|
845
|
+
for (const entry of patchEntries) {
|
|
846
|
+
if (__DEV__) {
|
|
847
|
+
console.log("[Connector] Processing PATCH entry individually:", {
|
|
848
|
+
table: entry.table,
|
|
849
|
+
op: entry.op,
|
|
850
|
+
id: entry.id,
|
|
851
|
+
opData: entry.opData
|
|
852
|
+
});
|
|
853
|
+
}
|
|
906
854
|
await this.processWithRetry(entry);
|
|
907
855
|
successfulEntries.push(entry);
|
|
908
|
-
} catch (error) {
|
|
909
|
-
criticalFailures.push({
|
|
910
|
-
entry,
|
|
911
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
912
|
-
});
|
|
913
856
|
}
|
|
914
857
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
table: f.entry.table,
|
|
935
|
-
op: f.entry.op,
|
|
936
|
-
id: f.entry.id
|
|
937
|
-
}))
|
|
858
|
+
await this.finalizeTransaction({
|
|
859
|
+
transaction,
|
|
860
|
+
successfulEntries
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Process batched PUT (upsert) operations for a single table.
|
|
865
|
+
* Falls back to individual processing if batch fails.
|
|
866
|
+
* CRITICAL: Throws on first failure to maintain transaction atomicity.
|
|
867
|
+
* @returns Array of successfully processed entries
|
|
868
|
+
* @throws Error on first failure - keeps entire transaction in ps_crud
|
|
869
|
+
*/
|
|
870
|
+
async processBatchedPuts(table, schema, entries) {
|
|
871
|
+
const successful = [];
|
|
872
|
+
if (__DEV__) {
|
|
873
|
+
console.log("[Connector] Processing batched PUTs:", {
|
|
874
|
+
schema,
|
|
875
|
+
table,
|
|
876
|
+
count: entries.length
|
|
938
877
|
});
|
|
939
|
-
this.onTransactionFailure?.(criticalFailures.map((f) => f.entry), firstFailure.error, classified);
|
|
940
|
-
throw firstFailure.error;
|
|
941
878
|
}
|
|
879
|
+
const rows = entries.map((entry) => ({
|
|
880
|
+
id: entry.id,
|
|
881
|
+
...entry.opData
|
|
882
|
+
}));
|
|
883
|
+
const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
|
|
884
|
+
const {
|
|
885
|
+
error
|
|
886
|
+
} = await query.upsert(rows, {
|
|
887
|
+
onConflict: "id"
|
|
888
|
+
});
|
|
889
|
+
if (error) {
|
|
890
|
+
if (__DEV__) {
|
|
891
|
+
console.warn("[Connector] Batched PUT failed, falling back to individual processing:", {
|
|
892
|
+
schema,
|
|
893
|
+
table,
|
|
894
|
+
error: error.message
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
await this.processWithRetry(entry);
|
|
899
|
+
successful.push(entry);
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
if (__DEV__) {
|
|
903
|
+
console.log("[Connector] Batched PUT successful:", {
|
|
904
|
+
schema,
|
|
905
|
+
table,
|
|
906
|
+
count: entries.length
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
successful.push(...entries);
|
|
910
|
+
}
|
|
911
|
+
return successful;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Process batched DELETE operations for a single table.
|
|
915
|
+
* Falls back to individual processing if batch fails.
|
|
916
|
+
* CRITICAL: Throws on first failure to maintain transaction atomicity.
|
|
917
|
+
* @returns Array of successfully processed entries
|
|
918
|
+
* @throws Error on first failure - keeps entire transaction in ps_crud
|
|
919
|
+
*/
|
|
920
|
+
async processBatchedDeletes(table, schema, entries) {
|
|
921
|
+
const successful = [];
|
|
942
922
|
if (__DEV__) {
|
|
943
|
-
console.log("[Connector]
|
|
923
|
+
console.log("[Connector] Processing batched DELETEs:", {
|
|
924
|
+
schema,
|
|
925
|
+
table,
|
|
926
|
+
count: entries.length
|
|
927
|
+
});
|
|
944
928
|
}
|
|
945
|
-
|
|
946
|
-
|
|
929
|
+
const ids = entries.map((entry) => entry.id);
|
|
930
|
+
const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
|
|
931
|
+
const {
|
|
932
|
+
error
|
|
933
|
+
} = await query.delete().in("id", ids);
|
|
934
|
+
if (error) {
|
|
947
935
|
if (__DEV__) {
|
|
948
|
-
console.
|
|
949
|
-
|
|
936
|
+
console.warn("[Connector] Batched DELETE failed, falling back to individual processing:", {
|
|
937
|
+
schema,
|
|
938
|
+
table,
|
|
939
|
+
error: error.message
|
|
950
940
|
});
|
|
951
941
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
id: e.id
|
|
966
|
-
}))
|
|
967
|
-
});
|
|
968
|
-
this.logger?.error("[Connector] Transaction completion error:", {
|
|
969
|
-
error,
|
|
970
|
-
classified,
|
|
971
|
-
entries: successfulEntries.map((e) => ({
|
|
972
|
-
table: e.table,
|
|
973
|
-
op: e.op,
|
|
974
|
-
id: e.id
|
|
975
|
-
}))
|
|
976
|
-
});
|
|
977
|
-
this.onTransactionFailure?.(successfulEntries, error instanceof Error ? error : new Error(String(error)), classified);
|
|
978
|
-
throw error;
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
await this.processWithRetry(entry);
|
|
944
|
+
successful.push(entry);
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
if (__DEV__) {
|
|
948
|
+
console.log("[Connector] Batched DELETE successful:", {
|
|
949
|
+
schema,
|
|
950
|
+
table,
|
|
951
|
+
count: entries.length
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
successful.push(...entries);
|
|
979
955
|
}
|
|
956
|
+
return successful;
|
|
980
957
|
}
|
|
981
958
|
/**
|
|
982
959
|
* Check if a table has a _version column (cached).
|
|
960
|
+
* P4.1: Uses Promise-based locking to prevent duplicate concurrent queries.
|
|
983
961
|
*/
|
|
984
962
|
async checkVersionColumn(table, db) {
|
|
985
963
|
if (this.versionColumnCache.has(table)) {
|
|
986
964
|
return this.versionColumnCache.get(table);
|
|
987
965
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
966
|
+
if (!this.versionColumnPromises.has(table)) {
|
|
967
|
+
this.versionColumnPromises.set(table, hasVersionColumn(table, db).then((result) => {
|
|
968
|
+
this.versionColumnCache.set(table, result);
|
|
969
|
+
this.versionColumnPromises.delete(table);
|
|
970
|
+
return result;
|
|
971
|
+
}).catch((error) => {
|
|
972
|
+
this.versionColumnPromises.delete(table);
|
|
973
|
+
throw error;
|
|
974
|
+
}));
|
|
975
|
+
}
|
|
976
|
+
return this.versionColumnPromises.get(table);
|
|
991
977
|
}
|
|
992
978
|
/**
|
|
993
979
|
* Filter opData to only include specified fields.
|
|
@@ -1003,12 +989,6 @@ var SupabaseConnector = class {
|
|
|
1003
989
|
}
|
|
1004
990
|
return filtered;
|
|
1005
991
|
}
|
|
1006
|
-
/**
|
|
1007
|
-
* Process a single CRUD operation.
|
|
1008
|
-
*
|
|
1009
|
-
* UUID-native tables (public schema, post-migration) use `id` as the UUID column.
|
|
1010
|
-
* Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.
|
|
1011
|
-
*/
|
|
1012
992
|
/**
|
|
1013
993
|
* Process a single CRUD operation.
|
|
1014
994
|
*
|
|
@@ -1018,6 +998,16 @@ var SupabaseConnector = class {
|
|
|
1018
998
|
const table = entry.table;
|
|
1019
999
|
const id = entry.id;
|
|
1020
1000
|
const schema = this.schemaRouter(table);
|
|
1001
|
+
if (entry.op === "PATCH" /* PATCH */ && Object.keys(entry.opData ?? {}).length === 0) {
|
|
1002
|
+
this.logger?.debug(`[Connector] Skipping empty PATCH for ${entry.table}:${entry.id}`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (entry.opData) {
|
|
1006
|
+
const payloadSize = JSON.stringify(entry.opData).length;
|
|
1007
|
+
if (payloadSize > MAX_PAYLOAD_SIZE) {
|
|
1008
|
+
throw new ValidationError(`Payload too large (${(payloadSize / 1024).toFixed(0)}KB > 900KB) for ${schema}.${table}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1021
1011
|
if (this.crudHandler) {
|
|
1022
1012
|
let handled = false;
|
|
1023
1013
|
switch (entry.op) {
|
|
@@ -1164,17 +1154,11 @@ var SupabaseConnector = class {
|
|
|
1164
1154
|
export {
|
|
1165
1155
|
defaultSchemaRouter,
|
|
1166
1156
|
DEFAULT_RETRY_CONFIG,
|
|
1157
|
+
ConflictDetectionError,
|
|
1167
1158
|
detectConflicts,
|
|
1168
1159
|
hasVersionColumn,
|
|
1169
1160
|
fetchServerVersion,
|
|
1170
1161
|
getLocalVersion,
|
|
1171
|
-
AbortError,
|
|
1172
|
-
RetryExhaustedError,
|
|
1173
|
-
calculateBackoffDelay,
|
|
1174
|
-
addJitter,
|
|
1175
|
-
sleep,
|
|
1176
|
-
DEFAULT_BACKOFF_CONFIG,
|
|
1177
|
-
withExponentialBackoff,
|
|
1178
1162
|
SupabaseConnector
|
|
1179
1163
|
};
|
|
1180
|
-
//# sourceMappingURL=chunk-
|
|
1164
|
+
//# sourceMappingURL=chunk-HFOFLW5F.js.map
|