@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
|
@@ -1,11 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withExponentialBackoff
|
|
3
|
+
} from "./chunk-FV2HXEIY.js";
|
|
1
4
|
import {
|
|
2
5
|
classifySupabaseError
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XQAJM2MW.js";
|
|
4
7
|
|
|
5
8
|
// src/connector/types.ts
|
|
6
9
|
var defaultSchemaRouter = () => "public";
|
|
10
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
11
|
+
transient: {
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
baseDelayMs: 1e3,
|
|
14
|
+
// 1 second initial delay
|
|
15
|
+
maxDelayMs: 4e3,
|
|
16
|
+
// 4 second cap
|
|
17
|
+
backoffMultiplier: 2
|
|
18
|
+
// 1s → 2s → 4s
|
|
19
|
+
},
|
|
20
|
+
permanent: {
|
|
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
|
|
29
|
+
}
|
|
30
|
+
};
|
|
7
31
|
|
|
8
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
|
+
}
|
|
9
68
|
var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
10
69
|
function validateTableName(table) {
|
|
11
70
|
if (!TABLE_NAME_REGEX.test(table)) {
|
|
@@ -37,14 +96,9 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
|
|
|
37
96
|
ascending: false
|
|
38
97
|
}).limit(20);
|
|
39
98
|
if (error) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
conflicts: [],
|
|
44
|
-
nonConflictingChanges: Object.keys(filteredPendingChanges),
|
|
45
|
-
table,
|
|
46
|
-
recordId
|
|
47
|
-
};
|
|
99
|
+
throw new ConflictDetectionError(`AuditLog query failed for ${table}/${recordId}`, {
|
|
100
|
+
cause: error
|
|
101
|
+
});
|
|
48
102
|
}
|
|
49
103
|
const serverChanges = /* @__PURE__ */ new Map();
|
|
50
104
|
for (const log of auditLogs ?? []) {
|
|
@@ -53,7 +107,7 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
|
|
|
53
107
|
if (!oldRec || !newRec) continue;
|
|
54
108
|
for (const [field, newValue] of Object.entries(newRec)) {
|
|
55
109
|
if (ignoredFields.has(field)) continue;
|
|
56
|
-
if (oldRec[field]
|
|
110
|
+
if (!deepEqual(oldRec[field], newValue) && !serverChanges.has(field)) {
|
|
57
111
|
serverChanges.set(field, {
|
|
58
112
|
newValue,
|
|
59
113
|
changedBy: log.changeBy,
|
|
@@ -113,6 +167,62 @@ async function getLocalVersion(table, recordId, db) {
|
|
|
113
167
|
}
|
|
114
168
|
|
|
115
169
|
// src/connector/supabase-connector.ts
|
|
170
|
+
var ValidationError = class extends Error {
|
|
171
|
+
constructor(message) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = "ValidationError";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
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;
|
|
182
|
+
}
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code === "401" || code === "403" || code === "42501") {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
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);
|
|
210
|
+
});
|
|
211
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
212
|
+
}
|
|
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]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return grouped;
|
|
225
|
+
}
|
|
116
226
|
var SupabaseConnector = class {
|
|
117
227
|
supabase;
|
|
118
228
|
powerSyncUrl;
|
|
@@ -136,6 +246,13 @@ var SupabaseConnector = class {
|
|
|
136
246
|
resolvedConflicts = /* @__PURE__ */ new Map();
|
|
137
247
|
// Cleanup function for resolution listener subscription
|
|
138
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;
|
|
253
|
+
// Retry configuration
|
|
254
|
+
retryConfig;
|
|
255
|
+
autoRetryPaused = false;
|
|
139
256
|
constructor(options) {
|
|
140
257
|
this.supabase = options.supabaseClient;
|
|
141
258
|
this.powerSyncUrl = options.powerSyncUrl;
|
|
@@ -149,6 +266,16 @@ var SupabaseConnector = class {
|
|
|
149
266
|
this.conflictDetection = options.conflictDetection;
|
|
150
267
|
this.conflictHandler = options.conflictHandler;
|
|
151
268
|
this.conflictBus = options.conflictBus;
|
|
269
|
+
this.retryConfig = {
|
|
270
|
+
transient: {
|
|
271
|
+
...DEFAULT_RETRY_CONFIG.transient,
|
|
272
|
+
...options.retryConfig?.transient
|
|
273
|
+
},
|
|
274
|
+
permanent: {
|
|
275
|
+
...DEFAULT_RETRY_CONFIG.permanent,
|
|
276
|
+
...options.retryConfig?.permanent
|
|
277
|
+
}
|
|
278
|
+
};
|
|
152
279
|
if (this.conflictBus) {
|
|
153
280
|
this.unsubscribeResolution = this.conflictBus.onResolution((table, recordId, resolution) => {
|
|
154
281
|
const key = `${table}:${recordId}`;
|
|
@@ -169,11 +296,153 @@ var SupabaseConnector = class {
|
|
|
169
296
|
* Call this when the connector is no longer needed.
|
|
170
297
|
*/
|
|
171
298
|
destroy() {
|
|
299
|
+
this.isDestroyed = true;
|
|
172
300
|
if (this.unsubscribeResolution) {
|
|
173
301
|
this.unsubscribeResolution();
|
|
174
302
|
this.unsubscribeResolution = void 0;
|
|
175
303
|
}
|
|
176
304
|
this.resolvedConflicts.clear();
|
|
305
|
+
this.versionColumnPromises.clear();
|
|
306
|
+
}
|
|
307
|
+
// ─── Retry Control Methods ─────────────────────────────────────────────────
|
|
308
|
+
/**
|
|
309
|
+
* Pause automatic retry of failed uploads.
|
|
310
|
+
* Use this when the user goes offline intentionally or wants manual control.
|
|
311
|
+
*/
|
|
312
|
+
pauseAutoRetry() {
|
|
313
|
+
this.autoRetryPaused = true;
|
|
314
|
+
if (__DEV__) {
|
|
315
|
+
console.log("[Connector] Auto-retry paused");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Resume automatic retry of failed uploads.
|
|
320
|
+
*/
|
|
321
|
+
resumeAutoRetry() {
|
|
322
|
+
this.autoRetryPaused = false;
|
|
323
|
+
if (__DEV__) {
|
|
324
|
+
console.log("[Connector] Auto-retry resumed");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// ─── Private Retry Logic ───────────────────────────────────────────────────
|
|
328
|
+
/**
|
|
329
|
+
* Process a single CRUD entry with exponential backoff retry.
|
|
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
|
+
*
|
|
338
|
+
* @param entry - The CRUD entry to process
|
|
339
|
+
* @throws Error if all retries exhausted (for critical failures)
|
|
340
|
+
*/
|
|
341
|
+
async processWithRetry(entry) {
|
|
342
|
+
if (this.isDestroyed) {
|
|
343
|
+
throw new Error("Connector destroyed");
|
|
344
|
+
}
|
|
345
|
+
const classified = {
|
|
346
|
+
isPermanent: false,
|
|
347
|
+
pgCode: void 0,
|
|
348
|
+
userMessage: ""
|
|
349
|
+
};
|
|
350
|
+
let lastError;
|
|
351
|
+
try {
|
|
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
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
await this.supabase.auth.refreshSession();
|
|
370
|
+
await this.processCrudEntry(entry);
|
|
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;
|
|
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);
|
|
402
|
+
}, selectedConfig, {
|
|
403
|
+
onRetry: (attempt, delay, error) => {
|
|
404
|
+
this.logger?.debug("[Connector] Retry attempt:", {
|
|
405
|
+
table: entry.table,
|
|
406
|
+
op: entry.op,
|
|
407
|
+
id: entry.id,
|
|
408
|
+
attempt,
|
|
409
|
+
delay,
|
|
410
|
+
error: error.message
|
|
411
|
+
});
|
|
412
|
+
if (__DEV__) {
|
|
413
|
+
console.log("[Connector] Retry attempt:", {
|
|
414
|
+
table: entry.table,
|
|
415
|
+
op: entry.op,
|
|
416
|
+
id: entry.id,
|
|
417
|
+
attempt,
|
|
418
|
+
maxRetries: selectedConfig.maxRetries,
|
|
419
|
+
delayMs: delay,
|
|
420
|
+
errorMessage: error.message
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
427
|
+
const category = classified.isPermanent ? "permanent" : classified.pgCode ? "transient" : "unknown";
|
|
428
|
+
if (__DEV__) {
|
|
429
|
+
console.log("[Connector] CRUD entry failed after retries, leaving in ps_crud:", {
|
|
430
|
+
table: entry.table,
|
|
431
|
+
op: entry.op,
|
|
432
|
+
id: entry.id,
|
|
433
|
+
category,
|
|
434
|
+
error: finalError.message
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
this.logger?.error("[Connector] CRUD entry failed after retries:", {
|
|
438
|
+
table: entry.table,
|
|
439
|
+
op: entry.op,
|
|
440
|
+
id: entry.id,
|
|
441
|
+
error: finalError.message,
|
|
442
|
+
category
|
|
443
|
+
});
|
|
444
|
+
throw finalError;
|
|
445
|
+
}
|
|
177
446
|
}
|
|
178
447
|
/**
|
|
179
448
|
* Set the active project IDs for scoped sync.
|
|
@@ -230,20 +499,33 @@ var SupabaseConnector = class {
|
|
|
230
499
|
* 5. Applies resolution or skips entry based on handler response
|
|
231
500
|
*/
|
|
232
501
|
async uploadData(database) {
|
|
502
|
+
if (this.isDestroyed) {
|
|
503
|
+
throw new Error("Connector destroyed - aborting upload");
|
|
504
|
+
}
|
|
233
505
|
if (this.shouldUploadFn && !this.shouldUploadFn()) {
|
|
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");
|
|
508
|
+
}
|
|
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");
|
|
234
517
|
if (__DEV__) {
|
|
235
|
-
console.
|
|
518
|
+
console.error("[Connector] uploadData failed: no session");
|
|
236
519
|
}
|
|
237
|
-
|
|
238
|
-
return;
|
|
520
|
+
throw noSessionError;
|
|
239
521
|
}
|
|
240
522
|
if (__DEV__) {
|
|
241
523
|
console.log("[Connector] uploadData called, fetching next CRUD transaction...");
|
|
242
524
|
}
|
|
243
525
|
const transaction = await database.getNextCrudTransaction();
|
|
244
|
-
if (!transaction) {
|
|
526
|
+
if (!transaction || transaction.crud.length === 0) {
|
|
245
527
|
if (__DEV__) {
|
|
246
|
-
console.log("[Connector] No pending CRUD transaction found");
|
|
528
|
+
console.log("[Connector] No pending CRUD transaction found or transaction is empty");
|
|
247
529
|
}
|
|
248
530
|
return;
|
|
249
531
|
}
|
|
@@ -300,6 +582,9 @@ var SupabaseConnector = class {
|
|
|
300
582
|
entriesDiscarded.push(entry);
|
|
301
583
|
break;
|
|
302
584
|
case "partial":
|
|
585
|
+
if (!existingResolution.fields || existingResolution.fields.length === 0) {
|
|
586
|
+
throw new Error("Partial resolution requires at least one field");
|
|
587
|
+
}
|
|
303
588
|
const partialEntry = {
|
|
304
589
|
...entry,
|
|
305
590
|
opData: this.filterFields(entry.opData ?? {}, existingResolution.fields)
|
|
@@ -354,6 +639,9 @@ var SupabaseConnector = class {
|
|
|
354
639
|
}
|
|
355
640
|
break;
|
|
356
641
|
case "partial":
|
|
642
|
+
if (!resolution.fields || resolution.fields.length === 0) {
|
|
643
|
+
throw new Error("Partial resolution requires at least one field");
|
|
644
|
+
}
|
|
357
645
|
const partialEntry = {
|
|
358
646
|
...entry,
|
|
359
647
|
opData: this.filterFields(entry.opData ?? {}, resolution.fields)
|
|
@@ -366,7 +654,7 @@ var SupabaseConnector = class {
|
|
|
366
654
|
break;
|
|
367
655
|
}
|
|
368
656
|
} else {
|
|
369
|
-
|
|
657
|
+
this.logger?.warn("[Connector] Conflict detected but no handler:", {
|
|
370
658
|
table: entry.table,
|
|
371
659
|
id: entry.id,
|
|
372
660
|
conflicts: conflictResult.conflicts
|
|
@@ -383,7 +671,7 @@ var SupabaseConnector = class {
|
|
|
383
671
|
});
|
|
384
672
|
}
|
|
385
673
|
this.onTransactionComplete?.(entriesQueuedForUI);
|
|
386
|
-
|
|
674
|
+
throw new Error("Entries queued for UI resolution - retry will occur on next sync cycle");
|
|
387
675
|
}
|
|
388
676
|
if (entriesToProcess.length === 0 && entriesDiscarded.length > 0) {
|
|
389
677
|
if (__DEV__) {
|
|
@@ -400,28 +688,62 @@ var SupabaseConnector = class {
|
|
|
400
688
|
}
|
|
401
689
|
return;
|
|
402
690
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (__DEV__) {
|
|
406
|
-
console.log("[Connector] Processing CRUD entry:", {
|
|
407
|
-
table: entry.table,
|
|
408
|
-
op: entry.op,
|
|
409
|
-
id: entry.id,
|
|
410
|
-
opData: entry.opData
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
await this.processCrudEntry(entry);
|
|
414
|
-
}
|
|
691
|
+
const successfulEntries = [];
|
|
692
|
+
for (const entry of entriesToProcess) {
|
|
415
693
|
if (__DEV__) {
|
|
416
|
-
console.log("[Connector]
|
|
694
|
+
console.log("[Connector] Processing CRUD entry with retry:", {
|
|
695
|
+
table: entry.table,
|
|
696
|
+
op: entry.op,
|
|
697
|
+
id: entry.id,
|
|
698
|
+
opData: entry.opData
|
|
699
|
+
});
|
|
417
700
|
}
|
|
418
|
-
await
|
|
701
|
+
await this.processWithRetry(entry);
|
|
702
|
+
successfulEntries.push(entry);
|
|
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;
|
|
724
|
+
if (__DEV__) {
|
|
725
|
+
console.log("[Connector] All CRUD entries processed, completing transaction...");
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
await withTimeout(transaction.complete(), 3e4, "Transaction complete timeout");
|
|
419
729
|
if (__DEV__) {
|
|
420
730
|
console.log("[Connector] Transaction completed successfully:", {
|
|
421
|
-
entriesCount:
|
|
422
|
-
discardedCount:
|
|
731
|
+
entriesCount: successfulEntries.length,
|
|
732
|
+
discardedCount: discardedEntries.length
|
|
423
733
|
});
|
|
424
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
|
+
}
|
|
425
747
|
if (this.conflictBus && partialResolutions.length > 0) {
|
|
426
748
|
for (const {
|
|
427
749
|
originalConflict,
|
|
@@ -448,100 +770,210 @@ var SupabaseConnector = class {
|
|
|
448
770
|
}
|
|
449
771
|
}
|
|
450
772
|
}
|
|
451
|
-
this.onTransactionSuccess?.(
|
|
452
|
-
this.onTransactionComplete?.(
|
|
773
|
+
this.onTransactionSuccess?.(successfulEntries);
|
|
774
|
+
this.onTransactionComplete?.(successfulEntries);
|
|
453
775
|
} catch (error) {
|
|
454
776
|
const classified = classifySupabaseError(error);
|
|
455
|
-
|
|
777
|
+
this.logger?.error("[Connector] Transaction completion FAILED:", {
|
|
456
778
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
457
|
-
errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
|
|
458
|
-
errorObject: JSON.stringify(error, null, 2),
|
|
459
779
|
classified,
|
|
460
780
|
isPermanent: classified.isPermanent,
|
|
461
|
-
entries:
|
|
781
|
+
entries: successfulEntries.map((e) => ({
|
|
462
782
|
table: e.table,
|
|
463
783
|
op: e.op,
|
|
464
784
|
id: e.id
|
|
465
785
|
}))
|
|
466
786
|
});
|
|
467
|
-
|
|
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
|
+
}
|
|
793
|
+
this.logger?.error("[Connector] Transaction completion error:", {
|
|
468
794
|
error,
|
|
469
795
|
classified,
|
|
470
|
-
entries:
|
|
796
|
+
entries: successfulEntries.map((e) => ({
|
|
471
797
|
table: e.table,
|
|
472
798
|
op: e.op,
|
|
473
799
|
id: e.id
|
|
474
800
|
}))
|
|
475
801
|
});
|
|
476
|
-
this.onTransactionFailure?.(
|
|
802
|
+
this.onTransactionFailure?.(successfulEntries, error instanceof Error ? error : new Error(String(error)), classified);
|
|
477
803
|
throw error;
|
|
478
804
|
}
|
|
479
805
|
}
|
|
480
806
|
/**
|
|
481
807
|
* Process a transaction without conflict detection.
|
|
482
|
-
*
|
|
808
|
+
* Uses batched operations for PUT and DELETE, individual processing for PATCH.
|
|
483
809
|
*/
|
|
484
810
|
async processTransaction(transaction, _database) {
|
|
485
|
-
|
|
486
|
-
|
|
811
|
+
const successfulEntries = [];
|
|
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;
|
|
836
|
+
}
|
|
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) {
|
|
487
846
|
if (__DEV__) {
|
|
488
|
-
console.log("[Connector] Processing
|
|
847
|
+
console.log("[Connector] Processing PATCH entry individually:", {
|
|
489
848
|
table: entry.table,
|
|
490
849
|
op: entry.op,
|
|
491
850
|
id: entry.id,
|
|
492
851
|
opData: entry.opData
|
|
493
852
|
});
|
|
494
853
|
}
|
|
495
|
-
await this.
|
|
854
|
+
await this.processWithRetry(entry);
|
|
855
|
+
successfulEntries.push(entry);
|
|
496
856
|
}
|
|
857
|
+
}
|
|
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
|
|
877
|
+
});
|
|
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) {
|
|
497
890
|
if (__DEV__) {
|
|
498
|
-
console.
|
|
891
|
+
console.warn("[Connector] Batched PUT failed, falling back to individual processing:", {
|
|
892
|
+
schema,
|
|
893
|
+
table,
|
|
894
|
+
error: error.message
|
|
895
|
+
});
|
|
499
896
|
}
|
|
500
|
-
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
await this.processWithRetry(entry);
|
|
899
|
+
successful.push(entry);
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
501
902
|
if (__DEV__) {
|
|
502
|
-
console.log("[Connector]
|
|
503
|
-
|
|
903
|
+
console.log("[Connector] Batched PUT successful:", {
|
|
904
|
+
schema,
|
|
905
|
+
table,
|
|
906
|
+
count: entries.length
|
|
504
907
|
});
|
|
505
908
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
classified,
|
|
525
|
-
entries: transaction.crud.map((e) => ({
|
|
526
|
-
table: e.table,
|
|
527
|
-
op: e.op,
|
|
528
|
-
id: e.id
|
|
529
|
-
}))
|
|
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 = [];
|
|
922
|
+
if (__DEV__) {
|
|
923
|
+
console.log("[Connector] Processing batched DELETEs:", {
|
|
924
|
+
schema,
|
|
925
|
+
table,
|
|
926
|
+
count: entries.length
|
|
530
927
|
});
|
|
531
|
-
this.onTransactionFailure?.(transaction.crud, error instanceof Error ? error : new Error(String(error)), classified);
|
|
532
|
-
throw error;
|
|
533
928
|
}
|
|
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) {
|
|
935
|
+
if (__DEV__) {
|
|
936
|
+
console.warn("[Connector] Batched DELETE failed, falling back to individual processing:", {
|
|
937
|
+
schema,
|
|
938
|
+
table,
|
|
939
|
+
error: error.message
|
|
940
|
+
});
|
|
941
|
+
}
|
|
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);
|
|
955
|
+
}
|
|
956
|
+
return successful;
|
|
534
957
|
}
|
|
535
958
|
/**
|
|
536
959
|
* Check if a table has a _version column (cached).
|
|
960
|
+
* P4.1: Uses Promise-based locking to prevent duplicate concurrent queries.
|
|
537
961
|
*/
|
|
538
962
|
async checkVersionColumn(table, db) {
|
|
539
963
|
if (this.versionColumnCache.has(table)) {
|
|
540
964
|
return this.versionColumnCache.get(table);
|
|
541
965
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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);
|
|
545
977
|
}
|
|
546
978
|
/**
|
|
547
979
|
* Filter opData to only include specified fields.
|
|
@@ -557,12 +989,6 @@ var SupabaseConnector = class {
|
|
|
557
989
|
}
|
|
558
990
|
return filtered;
|
|
559
991
|
}
|
|
560
|
-
/**
|
|
561
|
-
* Process a single CRUD operation.
|
|
562
|
-
*
|
|
563
|
-
* UUID-native tables (public schema, post-migration) use `id` as the UUID column.
|
|
564
|
-
* Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.
|
|
565
|
-
*/
|
|
566
992
|
/**
|
|
567
993
|
* Process a single CRUD operation.
|
|
568
994
|
*
|
|
@@ -572,6 +998,16 @@ var SupabaseConnector = class {
|
|
|
572
998
|
const table = entry.table;
|
|
573
999
|
const id = entry.id;
|
|
574
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
|
+
}
|
|
575
1011
|
if (this.crudHandler) {
|
|
576
1012
|
let handled = false;
|
|
577
1013
|
switch (entry.op) {
|
|
@@ -717,10 +1153,12 @@ var SupabaseConnector = class {
|
|
|
717
1153
|
|
|
718
1154
|
export {
|
|
719
1155
|
defaultSchemaRouter,
|
|
1156
|
+
DEFAULT_RETRY_CONFIG,
|
|
1157
|
+
ConflictDetectionError,
|
|
720
1158
|
detectConflicts,
|
|
721
1159
|
hasVersionColumn,
|
|
722
1160
|
fetchServerVersion,
|
|
723
1161
|
getLocalVersion,
|
|
724
1162
|
SupabaseConnector
|
|
725
1163
|
};
|
|
726
|
-
//# sourceMappingURL=chunk-
|
|
1164
|
+
//# sourceMappingURL=chunk-HFOFLW5F.js.map
|