supastash 0.2.8 → 0.2.10
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/desktop/utils/sync/pushLocal/uploadChunk.d.ts.map +1 -1
- package/dist/desktop/utils/sync/pushLocal/uploadChunk.js +54 -11
- package/dist/desktop/utils/sync/pushLocal/uploadHelpers.d.ts +2 -2
- package/dist/desktop/utils/sync/pushLocal/uploadHelpers.d.ts.map +1 -1
- package/dist/desktop/utils/sync/pushLocal/uploadHelpers.js +32 -2
- package/dist/native/utils/sync/pushLocal/uploadChunk.d.ts.map +1 -1
- package/dist/native/utils/sync/pushLocal/uploadChunk.js +53 -11
- package/dist/native/utils/sync/pushLocal/uploadHelpers.d.ts +2 -2
- package/dist/native/utils/sync/pushLocal/uploadHelpers.d.ts.map +1 -1
- package/dist/native/utils/sync/pushLocal/uploadHelpers.js +32 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pushLocal/uploadChunk.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pushLocal/uploadChunk.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AA6OnE;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,WAAW,EAAE,EAC9B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,iBAetD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CHUNK_SIZE } from "../../../../shared/constants/syncDefaults";
|
|
2
2
|
import { getSupastashConfig } from "../../../../shared/core/config";
|
|
3
|
+
import { isOnline } from "../../../../shared/utils/connection";
|
|
3
4
|
import { normalizeForSupabase } from "../../../../shared/utils/getSafeValues";
|
|
4
5
|
import log from "../../../../shared/utils/logs";
|
|
5
6
|
import { supabaseClientErr } from "../../../../shared/utils/supabaseClientErr";
|
|
@@ -28,6 +29,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
28
29
|
const hasRPCPath = !!config.pushRPCPath;
|
|
29
30
|
const ids = chunk.map((row) => row.id);
|
|
30
31
|
const toPush = [];
|
|
32
|
+
let remoteExistsMap = new Map();
|
|
31
33
|
// If we have a RPC path, we can push the whole chunk. Server validates freshness.
|
|
32
34
|
if (hasRPCPath) {
|
|
33
35
|
toPush.push(...chunk);
|
|
@@ -35,6 +37,8 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
35
37
|
else {
|
|
36
38
|
// Fetch remote data for the current chunk
|
|
37
39
|
const remoteIds = await fetchRemoteHeadsChunked(table, ids, supabase);
|
|
40
|
+
for (const id of ids)
|
|
41
|
+
remoteExistsMap.set(id, remoteIds.has(id));
|
|
38
42
|
// Loop through the initial chunk and check if the id is in the remote data
|
|
39
43
|
const filtered = filterRowsByUpdatedAt(table, chunk, remoteIds);
|
|
40
44
|
toPush.push(...filtered);
|
|
@@ -70,7 +74,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
70
74
|
let batchOk = false;
|
|
71
75
|
// RPC return values
|
|
72
76
|
let completed = [];
|
|
73
|
-
let existsMap = new Map();
|
|
77
|
+
let existsMap = new Map(remoteExistsMap);
|
|
74
78
|
if (onPushToRemote) {
|
|
75
79
|
const ok = await onPushToRemote(pending);
|
|
76
80
|
if (ok)
|
|
@@ -82,12 +86,43 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
82
86
|
pending = [...res.data.skipped];
|
|
83
87
|
existsMap = res.data.existsMap;
|
|
84
88
|
batchOk = res.error == null && pending.length === 0;
|
|
85
|
-
// If there was an RPC error, we need to retry the main function
|
|
86
89
|
if (res.error) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
if (!(await isOnline())) {
|
|
91
|
+
attempts++;
|
|
92
|
+
await backoff(attempts);
|
|
93
|
+
pending = [...preflightOK];
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Online: RPC failed — run per-row single upserts immediately, no retry.
|
|
97
|
+
// pending was reassigned to res.data.skipped (empty on error), so use preflightOK.
|
|
98
|
+
const rowsToProcess = [...preflightOK];
|
|
99
|
+
try {
|
|
100
|
+
const heads = await fetchRemoteHeadsChunked(table, rowsToProcess.map((r) => r.id), supabase);
|
|
101
|
+
for (const r of rowsToProcess)
|
|
102
|
+
existsMap.set(r.id, heads.has(r.id));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// existsMap stays empty — singleUpsert will fall back to upsert
|
|
106
|
+
}
|
|
107
|
+
const syncedNow = [];
|
|
108
|
+
const keep = [];
|
|
109
|
+
for (const row of rowsToProcess) {
|
|
110
|
+
const rowRes = await singleUpsert(table, row, supabase, existsMap);
|
|
111
|
+
if (!rowRes.error) {
|
|
112
|
+
syncedNow.push(row.id);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
errorCount++;
|
|
116
|
+
lastError = rowRes.error;
|
|
117
|
+
const decision = await handleRowFailure(config, table, row, rowRes.error, supabase);
|
|
118
|
+
if (decision !== "KEEP")
|
|
119
|
+
continue;
|
|
120
|
+
keep.push(row);
|
|
121
|
+
}
|
|
122
|
+
if (syncedNow.length)
|
|
123
|
+
await markSynced(table, syncedNow);
|
|
124
|
+
pending = keep;
|
|
125
|
+
break;
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
else {
|
|
@@ -115,7 +150,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
115
150
|
res = await rpcUpsertSingle({ table, row, supabase, existsMap });
|
|
116
151
|
}
|
|
117
152
|
else {
|
|
118
|
-
res = await singleUpsert(table, row, supabase);
|
|
153
|
+
res = await singleUpsert(table, row, supabase, existsMap);
|
|
119
154
|
}
|
|
120
155
|
if (!res.error) {
|
|
121
156
|
syncedNow.push(row.id);
|
|
@@ -133,10 +168,18 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
133
168
|
await markSynced(table, syncedNow);
|
|
134
169
|
if (keep.length === 0)
|
|
135
170
|
return;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
if (!(await isOnline())) {
|
|
172
|
+
attempts++;
|
|
173
|
+
await backoff(attempts);
|
|
174
|
+
pending = keep;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Online: errors are genuine failures, not network issues — don't retry
|
|
178
|
+
for (const r of keep)
|
|
179
|
+
setQueryStatus(r.id, table, "error");
|
|
180
|
+
pending = keep; // update pending so post-loop markLogError reflects only true failures
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
140
183
|
}
|
|
141
184
|
if (pending.length > 0) {
|
|
142
185
|
SyncInfoUpdater.markLogError({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SupastashConfig } from "../../../../shared/types/supastashConfig.types";
|
|
2
2
|
import { RowLike } from "../../../../shared/types/syncEngine.types";
|
|
3
|
-
export declare function classifyFailure(cfg: SupastashConfig<any>, code?: string | number): "HTTP" | "UNKNOWN" | "NON_RETRYABLE" | "FK_BLOCK" | "RETRYABLE";
|
|
3
|
+
export declare function classifyFailure(cfg: SupastashConfig<any>, code?: string | number): "HTTP" | "UNKNOWN" | "NON_RETRYABLE" | "FK_BLOCK" | "UNIQUE_VIOLATION" | "RETRYABLE";
|
|
4
4
|
declare function batchUpsert(table: string, rows: RowLike[], supabase: any): Promise<any>;
|
|
5
|
-
declare function singleUpsert(table: string, row: RowLike, supabase: any): Promise<any>;
|
|
5
|
+
declare function singleUpsert(table: string, row: RowLike, supabase: any, existsMap?: Map<string, boolean>): Promise<any>;
|
|
6
6
|
declare function backoff(attempts: number): Promise<void>;
|
|
7
7
|
declare function rpcUpsert({ table, rows, supabase, }: {
|
|
8
8
|
table: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadHelpers.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pushLocal/uploadHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAC;AAOpE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"uploadHelpers.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pushLocal/uploadHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAC;AAOpE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,wFAYvB;AAED,iBAAe,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,gBAEvE;AAED,iBAAe,YAAY,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,GAAG,EACb,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,gBAwBjC;AAED,iBAAe,OAAO,CAAC,QAAQ,EAAE,MAAM,iBAOtC;AAmBD,iBAAe,SAAS,CAAC,EACvB,KAAK,EACL,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,QAAQ,EAAE,GAAG,CAAC;CACf;;;;;;;GA+CA;AAED,iBAAe,eAAe,CAAC,EAC7B,KAAK,EACL,GAAG,EACH,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,QAAQ,EAAE,GAAG,CAAC;IACd,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;;;;;;GAaA;AAMD,iBAAe,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,iBAIrD;AAWD,iBAAS,qBAAqB,CAC5B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EAAE,EAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,aAiCjC;AAMD,iBAAe,gBAAgB,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,GAAG,GACZ,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC,CA4DvC;AAgBD,OAAO,EACL,OAAO,EACP,WAAW,EACX,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,eAAe,EACf,YAAY,GACb,CAAC;AAEF;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,GAAG,iBAkBd;AASD,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EAAE,EACb,QAAQ,EAAE,GAAG,gCAcd"}
|
|
@@ -17,6 +17,8 @@ export function classifyFailure(cfg, code) {
|
|
|
17
17
|
return "NON_RETRYABLE";
|
|
18
18
|
if (s === (p.fkCode ?? "23503"))
|
|
19
19
|
return "FK_BLOCK";
|
|
20
|
+
if (s === "23505")
|
|
21
|
+
return "UNIQUE_VIOLATION";
|
|
20
22
|
if (p.retryableCodes?.has?.(s))
|
|
21
23
|
return "RETRYABLE";
|
|
22
24
|
return "UNKNOWN";
|
|
@@ -24,8 +26,30 @@ export function classifyFailure(cfg, code) {
|
|
|
24
26
|
async function batchUpsert(table, rows, supabase) {
|
|
25
27
|
return await supabase.from(table).upsert(rows);
|
|
26
28
|
}
|
|
27
|
-
async function singleUpsert(table, row, supabase) {
|
|
28
|
-
|
|
29
|
+
async function singleUpsert(table, row, supabase, existsMap) {
|
|
30
|
+
const exists = existsMap?.get(row.id);
|
|
31
|
+
if (exists === true) {
|
|
32
|
+
const { data, error } = await supabase
|
|
33
|
+
.from(table)
|
|
34
|
+
.update(row)
|
|
35
|
+
.eq("id", row.id)
|
|
36
|
+
.select("id")
|
|
37
|
+
.maybeSingle();
|
|
38
|
+
if (!error)
|
|
39
|
+
return { data, error: null };
|
|
40
|
+
// RLS may block update — fall through to upsert
|
|
41
|
+
}
|
|
42
|
+
else if (exists === false) {
|
|
43
|
+
const { data, error } = await supabase
|
|
44
|
+
.from(table)
|
|
45
|
+
.insert(row)
|
|
46
|
+
.select("id")
|
|
47
|
+
.maybeSingle();
|
|
48
|
+
if (!error)
|
|
49
|
+
return { data, error: null };
|
|
50
|
+
// RLS may block insert — fall through to upsert
|
|
51
|
+
}
|
|
52
|
+
return supabase.from(table).upsert(row).select("id").maybeSingle();
|
|
29
53
|
}
|
|
30
54
|
async function backoff(attempts) {
|
|
31
55
|
const config = getSupastashConfig();
|
|
@@ -170,6 +194,12 @@ async function handleRowFailure(cfg, table, row, err, supabase) {
|
|
|
170
194
|
return "DROP";
|
|
171
195
|
}
|
|
172
196
|
}
|
|
197
|
+
if (klass === "UNIQUE_VIOLATION") {
|
|
198
|
+
logWarn(`[Supastash] Row ${row.id} on ${table} violated a unique constraint (23505) → deleting local copy`, JSON.stringify(err));
|
|
199
|
+
await deleteLocalRow(table, row.id, supabase);
|
|
200
|
+
cfg.syncPolicy?.onRowDroppedLocal?.(table, row.id);
|
|
201
|
+
return "DROP";
|
|
202
|
+
}
|
|
173
203
|
if (klass === "FK_BLOCK") {
|
|
174
204
|
// Parent missing (23503) -> KEEP for later;
|
|
175
205
|
log(`Row ${row.id} on ${table} blocked by missing parent (FK) → will retry`, JSON.stringify(err));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pushLocal/uploadChunk.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pushLocal/uploadChunk.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAgPnE;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,WAAW,EAAE,EAC9B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,iBAetD"}
|
|
@@ -32,6 +32,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
32
32
|
const hasRPCPath = !!config.pushRPCPath;
|
|
33
33
|
const ids = chunk.map((row) => row.id);
|
|
34
34
|
const toPush = [];
|
|
35
|
+
let remoteExistsMap = new Map();
|
|
35
36
|
// If we have a RPC path, we can push the whole chunk. Server validates freshness.
|
|
36
37
|
if (hasRPCPath) {
|
|
37
38
|
toPush.push(...chunk);
|
|
@@ -39,6 +40,8 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
39
40
|
else {
|
|
40
41
|
// Fetch remote data for the current chunk
|
|
41
42
|
const remoteIds = await fetchRemoteHeadsChunked(table, ids, supabase);
|
|
43
|
+
for (const id of ids)
|
|
44
|
+
remoteExistsMap.set(id, remoteIds.has(id));
|
|
42
45
|
// Loop through the initial chunk and check if the id is in the remote data
|
|
43
46
|
const filtered = filterRowsByUpdatedAt(table, chunk, remoteIds);
|
|
44
47
|
toPush.push(...filtered);
|
|
@@ -74,7 +77,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
74
77
|
let batchOk = false;
|
|
75
78
|
// RPC return values
|
|
76
79
|
let completed = [];
|
|
77
|
-
let existsMap = new Map();
|
|
80
|
+
let existsMap = new Map(remoteExistsMap);
|
|
78
81
|
if (onPushToRemote) {
|
|
79
82
|
const ok = await onPushToRemote(pending);
|
|
80
83
|
if (ok)
|
|
@@ -86,12 +89,43 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
86
89
|
pending = [...res.data.skipped];
|
|
87
90
|
existsMap = res.data.existsMap;
|
|
88
91
|
batchOk = res.error == null && pending.length === 0;
|
|
89
|
-
// If there was an RPC error, we need to retry the main function
|
|
90
92
|
if (res.error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
if (!(await isOnline())) {
|
|
94
|
+
attempts++;
|
|
95
|
+
await backoff(attempts);
|
|
96
|
+
pending = [...preflightOK];
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Online: RPC failed — run per-row single upserts immediately, no retry.
|
|
100
|
+
// pending was reassigned to res.data.skipped (empty on error), so use preflightOK.
|
|
101
|
+
const rowsToProcess = [...preflightOK];
|
|
102
|
+
try {
|
|
103
|
+
const heads = await fetchRemoteHeadsChunked(table, rowsToProcess.map((r) => r.id), supabase);
|
|
104
|
+
for (const r of rowsToProcess)
|
|
105
|
+
existsMap.set(r.id, heads.has(r.id));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// existsMap stays empty — singleUpsert will fall back to upsert
|
|
109
|
+
}
|
|
110
|
+
const syncedNow = [];
|
|
111
|
+
const keep = [];
|
|
112
|
+
for (const row of rowsToProcess) {
|
|
113
|
+
const rowRes = await singleUpsert(table, row, supabase, existsMap);
|
|
114
|
+
if (!rowRes.error) {
|
|
115
|
+
syncedNow.push(row.id);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
errorCount++;
|
|
119
|
+
lastError = rowRes.error;
|
|
120
|
+
const decision = await handleRowFailure(config, table, row, rowRes.error, supabase);
|
|
121
|
+
if (decision !== "KEEP")
|
|
122
|
+
continue;
|
|
123
|
+
keep.push(row);
|
|
124
|
+
}
|
|
125
|
+
if (syncedNow.length)
|
|
126
|
+
await markSynced(table, syncedNow);
|
|
127
|
+
pending = keep;
|
|
128
|
+
break;
|
|
95
129
|
}
|
|
96
130
|
}
|
|
97
131
|
else {
|
|
@@ -119,7 +153,7 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
119
153
|
res = await rpcUpsertSingle({ table, row, supabase, existsMap });
|
|
120
154
|
}
|
|
121
155
|
else {
|
|
122
|
-
res = await singleUpsert(table, row, supabase);
|
|
156
|
+
res = await singleUpsert(table, row, supabase, existsMap);
|
|
123
157
|
}
|
|
124
158
|
if (!res.error) {
|
|
125
159
|
syncedNow.push(row.id);
|
|
@@ -137,10 +171,18 @@ async function uploadChunk(table, chunk, onPushToRemote) {
|
|
|
137
171
|
await markSynced(table, syncedNow);
|
|
138
172
|
if (keep.length === 0)
|
|
139
173
|
return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
174
|
+
if (!(await isOnline())) {
|
|
175
|
+
attempts++;
|
|
176
|
+
await backoff(attempts);
|
|
177
|
+
pending = keep;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Online: errors are genuine failures, not network issues — don't retry
|
|
181
|
+
for (const r of keep)
|
|
182
|
+
setQueryStatus(r.id, table, "error");
|
|
183
|
+
pending = keep; // update pending so post-loop markLogError reflects only true failures
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
144
186
|
}
|
|
145
187
|
if (pending.length > 0) {
|
|
146
188
|
SyncInfoUpdater.markLogError({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SupastashConfig } from "../../../../shared/types/supastashConfig.types";
|
|
2
2
|
import { RowLike } from "../../../../shared/types/syncEngine.types";
|
|
3
|
-
export declare function classifyFailure(cfg: SupastashConfig<any>, code?: string | number): "HTTP" | "UNKNOWN" | "NON_RETRYABLE" | "FK_BLOCK" | "RETRYABLE";
|
|
3
|
+
export declare function classifyFailure(cfg: SupastashConfig<any>, code?: string | number): "HTTP" | "UNKNOWN" | "NON_RETRYABLE" | "FK_BLOCK" | "UNIQUE_VIOLATION" | "RETRYABLE";
|
|
4
4
|
declare function batchUpsert(table: string, rows: RowLike[], supabase: any): Promise<any>;
|
|
5
|
-
declare function singleUpsert(table: string, row: RowLike, supabase: any): Promise<any>;
|
|
5
|
+
declare function singleUpsert(table: string, row: RowLike, supabase: any, existsMap?: Map<string, boolean>): Promise<any>;
|
|
6
6
|
declare function backoff(attempts: number): Promise<void>;
|
|
7
7
|
declare function rpcUpsert({ table, rows, supabase, }: {
|
|
8
8
|
table: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadHelpers.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pushLocal/uploadHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAC;AAOpE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"uploadHelpers.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pushLocal/uploadHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAC;AAOpE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,wFAYvB;AAED,iBAAe,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,gBAEvE;AAED,iBAAe,YAAY,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,GAAG,EACb,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,gBAwBjC;AAED,iBAAe,OAAO,CAAC,QAAQ,EAAE,MAAM,iBAOtC;AAmBD,iBAAe,SAAS,CAAC,EACvB,KAAK,EACL,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,QAAQ,EAAE,GAAG,CAAC;CACf;;;;;;;GA+CA;AAED,iBAAe,eAAe,CAAC,EAC7B,KAAK,EACL,GAAG,EACH,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,QAAQ,EAAE,GAAG,CAAC;IACd,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;;;;;;GAaA;AAMD,iBAAe,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,iBAIrD;AAWD,iBAAS,qBAAqB,CAC5B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EAAE,EAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,aAiCjC;AAMD,iBAAe,gBAAgB,CAC7B,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,GAAG,GACZ,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC,CA4DvC;AAgBD,OAAO,EACL,OAAO,EACP,WAAW,EACX,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,eAAe,EACf,YAAY,GACb,CAAC;AAEF;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,GAAG,iBAkBd;AASD,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EAAE,EACb,QAAQ,EAAE,GAAG,gCAcd"}
|
|
@@ -17,6 +17,8 @@ export function classifyFailure(cfg, code) {
|
|
|
17
17
|
return "NON_RETRYABLE";
|
|
18
18
|
if (s === (p.fkCode ?? "23503"))
|
|
19
19
|
return "FK_BLOCK";
|
|
20
|
+
if (s === "23505")
|
|
21
|
+
return "UNIQUE_VIOLATION";
|
|
20
22
|
if (p.retryableCodes?.has?.(s))
|
|
21
23
|
return "RETRYABLE";
|
|
22
24
|
return "UNKNOWN";
|
|
@@ -24,8 +26,30 @@ export function classifyFailure(cfg, code) {
|
|
|
24
26
|
async function batchUpsert(table, rows, supabase) {
|
|
25
27
|
return await supabase.from(table).upsert(rows);
|
|
26
28
|
}
|
|
27
|
-
async function singleUpsert(table, row, supabase) {
|
|
28
|
-
|
|
29
|
+
async function singleUpsert(table, row, supabase, existsMap) {
|
|
30
|
+
const exists = existsMap?.get(row.id);
|
|
31
|
+
if (exists === true) {
|
|
32
|
+
const { data, error } = await supabase
|
|
33
|
+
.from(table)
|
|
34
|
+
.update(row)
|
|
35
|
+
.eq("id", row.id)
|
|
36
|
+
.select("id")
|
|
37
|
+
.maybeSingle();
|
|
38
|
+
if (!error)
|
|
39
|
+
return { data, error: null };
|
|
40
|
+
// RLS may block update — fall through to upsert
|
|
41
|
+
}
|
|
42
|
+
else if (exists === false) {
|
|
43
|
+
const { data, error } = await supabase
|
|
44
|
+
.from(table)
|
|
45
|
+
.insert(row)
|
|
46
|
+
.select("id")
|
|
47
|
+
.maybeSingle();
|
|
48
|
+
if (!error)
|
|
49
|
+
return { data, error: null };
|
|
50
|
+
// RLS may block insert — fall through to upsert
|
|
51
|
+
}
|
|
52
|
+
return supabase.from(table).upsert(row).select("id").maybeSingle();
|
|
29
53
|
}
|
|
30
54
|
async function backoff(attempts) {
|
|
31
55
|
const config = getSupastashConfig();
|
|
@@ -170,6 +194,12 @@ async function handleRowFailure(cfg, table, row, err, supabase) {
|
|
|
170
194
|
return "DROP";
|
|
171
195
|
}
|
|
172
196
|
}
|
|
197
|
+
if (klass === "UNIQUE_VIOLATION") {
|
|
198
|
+
logWarn(`[Supastash] Row ${row.id} on ${table} violated a unique constraint (23505) → deleting local copy`, JSON.stringify(err));
|
|
199
|
+
await deleteLocalRow(table, row.id, supabase);
|
|
200
|
+
cfg.syncPolicy?.onRowDroppedLocal?.(table, row.id);
|
|
201
|
+
return "DROP";
|
|
202
|
+
}
|
|
173
203
|
if (klass === "FK_BLOCK") {
|
|
174
204
|
// Parent missing (23503) -> KEEP for later;
|
|
175
205
|
log(`Row ${row.id} on ${table} blocked by missing parent (FK) → will retry`, JSON.stringify(err));
|