supastash 0.2.9 → 0.2.11

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.
Files changed (60) hide show
  1. package/dist/desktop/hooks/syncEngine.d.ts.map +1 -1
  2. package/dist/desktop/hooks/syncEngine.js +7 -2
  3. package/dist/desktop/index.d.ts +3 -2
  4. package/dist/desktop/index.d.ts.map +1 -1
  5. package/dist/desktop/index.js +1 -0
  6. package/dist/desktop/utils/sync/pullFromRemote/index.d.ts +2 -1
  7. package/dist/desktop/utils/sync/pullFromRemote/index.d.ts.map +1 -1
  8. package/dist/desktop/utils/sync/pullFromRemote/index.js +3 -2
  9. package/dist/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.d.ts +9 -0
  10. package/dist/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.d.ts.map +1 -0
  11. package/dist/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.js +202 -0
  12. package/dist/desktop/utils/sync/pushLocal/uploadChunk.d.ts.map +1 -1
  13. package/dist/desktop/utils/sync/pushLocal/uploadChunk.js +54 -11
  14. package/dist/desktop/utils/sync/pushLocal/uploadHelpers.d.ts +1 -1
  15. package/dist/desktop/utils/sync/pushLocal/uploadHelpers.d.ts.map +1 -1
  16. package/dist/desktop/utils/sync/pushLocal/uploadHelpers.js +24 -2
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/native/hooks/syncEngine.d.ts.map +1 -1
  21. package/dist/native/hooks/syncEngine.js +7 -2
  22. package/dist/native/index.d.ts +1 -0
  23. package/dist/native/index.d.ts.map +1 -1
  24. package/dist/native/index.js +1 -0
  25. package/dist/native/utils/sync/pullFromRemote/index.d.ts +2 -1
  26. package/dist/native/utils/sync/pullFromRemote/index.d.ts.map +1 -1
  27. package/dist/native/utils/sync/pullFromRemote/index.js +7 -1
  28. package/dist/native/utils/sync/pullFromRemote/pullFromRemoteBatch.d.ts +9 -0
  29. package/dist/native/utils/sync/pullFromRemote/pullFromRemoteBatch.d.ts.map +1 -0
  30. package/dist/native/utils/sync/pullFromRemote/pullFromRemoteBatch.js +183 -0
  31. package/dist/native/utils/sync/pushLocal/uploadChunk.d.ts.map +1 -1
  32. package/dist/native/utils/sync/pushLocal/uploadChunk.js +53 -11
  33. package/dist/native/utils/sync/pushLocal/uploadHelpers.d.ts +1 -1
  34. package/dist/native/utils/sync/pushLocal/uploadHelpers.d.ts.map +1 -1
  35. package/dist/native/utils/sync/pushLocal/uploadHelpers.js +24 -2
  36. package/dist/shared/core/config/index.d.ts.map +1 -1
  37. package/dist/shared/core/config/index.js +2 -0
  38. package/dist/shared/hooks/supastashFilters/index.d.ts +9 -4
  39. package/dist/shared/hooks/supastashFilters/index.d.ts.map +1 -1
  40. package/dist/shared/hooks/supastashFilters/index.js +13 -5
  41. package/dist/shared/store/rpcTableFilters.d.ts +7 -0
  42. package/dist/shared/store/rpcTableFilters.d.ts.map +1 -0
  43. package/dist/shared/store/rpcTableFilters.js +5 -0
  44. package/dist/shared/types/rpcFilter.types.d.ts +23 -0
  45. package/dist/shared/types/supastashConfig.types.d.ts +38 -10
  46. package/dist/shared/utils/schema/createSyncStatus.d.ts.map +1 -1
  47. package/dist/shared/utils/schema/createSyncStatus.js +5 -1
  48. package/dist/shared/utils/sync/pullFromRemote/postgrestToRpc.d.ts +9 -0
  49. package/dist/shared/utils/sync/pullFromRemote/postgrestToRpc.d.ts.map +1 -0
  50. package/dist/shared/utils/sync/pullFromRemote/postgrestToRpc.js +50 -0
  51. package/dist/shared/utils/sync/pullFromRemote/updateFilter.d.ts +8 -5
  52. package/dist/shared/utils/sync/pullFromRemote/updateFilter.d.ts.map +1 -1
  53. package/dist/shared/utils/sync/pullFromRemote/updateFilter.js +11 -5
  54. package/dist/shared/utils/sync/pullFromRemote/updateRpcFilters.d.ts +12 -0
  55. package/dist/shared/utils/sync/pullFromRemote/updateRpcFilters.d.ts.map +1 -0
  56. package/dist/shared/utils/sync/pullFromRemote/updateRpcFilters.js +36 -0
  57. package/dist/shared/utils/sync/status/remoteSchema.d.ts +12 -0
  58. package/dist/shared/utils/sync/status/remoteSchema.d.ts.map +1 -1
  59. package/dist/shared/utils/sync/status/remoteSchema.js +46 -0
  60. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Batch pull: fetches all tables in a single RPC call per round,
3
+ * looping until `remaining_tables` is empty.
4
+ *
5
+ * Requires `useBatchPullSync: true` in config and the
6
+ * `supastash_pull_sync` Postgres function to be deployed.
7
+ */
8
+ export declare function pullFromRemoteBatch(): Promise<void>;
9
+ //# sourceMappingURL=pullFromRemoteBatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+KzD"}
@@ -0,0 +1,183 @@
1
+ import { getSupastashConfig } from "../../../../shared/core/config";
2
+ import { getSupastashDb } from "../../../../shared/db/dbInitializer";
3
+ import { rpcTableFilters } from "../../../../shared/store/rpcTableFilters";
4
+ import { tableFilters } from "../../../../shared/store/tableFilters";
5
+ import { postgrestFiltersToRpc } from "../../../../shared/utils/sync/pullFromRemote/postgrestToRpc";
6
+ import { getAllTables } from "../../../../shared/utils/sync/getAllTables";
7
+ import { getMaxSyncLookBack, logNoUpdates, returnMaxDate, } from "../../../../shared/utils/sync/pullFromRemote/helpers";
8
+ import { SyncInfoUpdater } from "../../../../shared/utils/sync/queryStatus";
9
+ import { refreshScreen } from "../../../../shared/utils/refreshScreenCalls";
10
+ import log, { logError, logWarn } from "../../../../shared/utils/logs";
11
+ import { supabaseClientErr } from "../../../../shared/utils/supabaseClientErr";
12
+ import { prefetchRemoteTableSchemas } from "../../../../shared/utils/sync/status/remoteSchema";
13
+ import { setSupastashSyncStatus } from "../status/services";
14
+ import { selectSyncStatus } from "../status/repo";
15
+ import { upsertChunkData } from "./updateLocalDb";
16
+ const CHUNK_SIZE = 999;
17
+ function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
18
+ if (lastPk) {
19
+ return {
20
+ or: [
21
+ { col: tsCol, op: "gt", val: lastSyncedAt },
22
+ {
23
+ and: [
24
+ { col: tsCol, op: "eq", val: lastSyncedAt },
25
+ { col: "id", op: "gt", val: lastPk },
26
+ ],
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ return { col: tsCol, op: "gte", val: lastSyncedAt };
32
+ }
33
+ /**
34
+ * Batch pull: fetches all tables in a single RPC call per round,
35
+ * looping until `remaining_tables` is empty.
36
+ *
37
+ * Requires `useBatchPullSync: true` in config and the
38
+ * `supastash_pull_sync` Postgres function to be deployed.
39
+ */
40
+ export async function pullFromRemoteBatch() {
41
+ const cfg = getSupastashConfig();
42
+ const supabase = cfg.supabaseClient;
43
+ if (!supabase)
44
+ throw new Error(`No supabase client found: ${supabaseClientErr}`);
45
+ if (cfg.supastashMode === "ghost")
46
+ return;
47
+ const tables = await getAllTables();
48
+ if (!tables) {
49
+ log("[Supastash] Batch pull: no tables found");
50
+ return;
51
+ }
52
+ const excludeTables = cfg.excludeTables?.pull ?? [];
53
+ const tablesToPull = tables.filter((t) => !excludeTables.includes(t));
54
+ if (!tablesToPull.length)
55
+ return;
56
+ const tsCol = cfg.replicationMode === "server-side" ? "arrived_at" : "updated_at";
57
+ const db = await getSupastashDb();
58
+ const completedTables = new Set();
59
+ SyncInfoUpdater.setInProgress({ action: "start", type: "pull" });
60
+ SyncInfoUpdater.setNumberOfTables({ amount: tablesToPull.length, type: "pull" });
61
+ // Warm schema cache for all tables in one call if enabled
62
+ if (cfg.useBatchSchemaFetch) {
63
+ await prefetchRemoteTableSchemas(tablesToPull);
64
+ }
65
+ let remainingTables = tablesToPull;
66
+ try {
67
+ while (remainingTables.length > 0) {
68
+ // ── Build per-table filters: base filters + cursor ──────────────────
69
+ const p_filters = {};
70
+ for (const table of remainingTables) {
71
+ const syncStatus = await selectSyncStatus(db, table, tableFilters.get(table) ?? []);
72
+ // Mirror pageThrough: cap the cursor to maxSyncLookbackDays so a
73
+ // first-time sync doesn't try to pull decades of data.
74
+ // fullSyncTables bypass the cap (getMaxSyncLookBack returns undefined).
75
+ const maxLookBack = getMaxSyncLookBack({ table });
76
+ const effectiveSince = maxLookBack &&
77
+ Date.parse(syncStatus.last_synced_at) < Date.parse(maxLookBack)
78
+ ? maxLookBack
79
+ : syncStatus.last_synced_at;
80
+ const cursorFilter = buildCursorFilter(tsCol, effectiveSince, syncStatus.last_synced_at_pk);
81
+ // Merge explicit RPC filters + PostgREST filters auto-converted at query time
82
+ const rpcBase = rpcTableFilters.get(table) ?? [];
83
+ const converted = postgrestFiltersToRpc(tableFilters.get(table));
84
+ const baseFilters = [...rpcBase, ...converted];
85
+ p_filters[table] = [...baseFilters, cursorFilter];
86
+ }
87
+ // ── Single RPC call ─────────────────────────────────────────────────
88
+ const { data, error } = await supabase.rpc("supastash_pull_sync", {
89
+ p_tables: remainingTables,
90
+ p_filters,
91
+ p_ts_col: tsCol,
92
+ });
93
+ if (error)
94
+ throw error;
95
+ const result = data;
96
+ const nextRemaining = result.remaining_tables ?? [];
97
+ // ── Process each table ──────────────────────────────────────────────
98
+ for (const [table, rows] of Object.entries(result.tables ?? {})) {
99
+ SyncInfoUpdater.markLogStart({ type: "pull", table });
100
+ try {
101
+ if (!rows?.length) {
102
+ logNoUpdates(table);
103
+ SyncInfoUpdater.markLogSuccess({ type: "pull", table });
104
+ continue;
105
+ }
106
+ const toDelete = [];
107
+ const toUpsert = [];
108
+ let prevMaxSyncedAt = null;
109
+ let prevMaxDeletedAt = null;
110
+ for (const row of rows) {
111
+ if (!row?.id) {
112
+ logWarn(`[Supastash] Batch: skipped row without id from "${table}"`);
113
+ continue;
114
+ }
115
+ prevMaxSyncedAt = returnMaxDate({ row, prevMax: prevMaxSyncedAt, col: tsCol });
116
+ prevMaxDeletedAt = returnMaxDate({ row, prevMax: prevMaxDeletedAt, col: "deleted_at" });
117
+ if (row.deleted_at) {
118
+ toDelete.push(row.id);
119
+ }
120
+ else {
121
+ toUpsert.push(row);
122
+ }
123
+ }
124
+ SyncInfoUpdater.setUnsyncedDataCount({ amount: toUpsert.length, type: "pull", table });
125
+ SyncInfoUpdater.setUnsyncedDeletedCount({ amount: toDelete.length, type: "pull", table });
126
+ // Delete soft-deleted rows
127
+ if (toDelete.length > 0) {
128
+ for (let i = 0; i < toDelete.length; i += CHUNK_SIZE) {
129
+ const slice = toDelete.slice(i, i + CHUNK_SIZE);
130
+ const placeholders = slice.map(() => "?").join(", ");
131
+ await db.runAsync(`DELETE FROM ${table} WHERE id IN (${placeholders})`, slice);
132
+ }
133
+ }
134
+ // Upsert live rows
135
+ if (toUpsert.length > 0) {
136
+ await upsertChunkData({ table, records: toUpsert });
137
+ }
138
+ // Update sync cursor so the next round starts from the right place
139
+ if (prevMaxSyncedAt || prevMaxDeletedAt) {
140
+ await setSupastashSyncStatus(table, tableFilters.get(table) ?? [], {
141
+ lastSyncedAt: prevMaxSyncedAt?.value ?? undefined,
142
+ lastDeletedAt: prevMaxDeletedAt?.value ?? undefined,
143
+ lastSyncedAtPk: prevMaxSyncedAt?.pk ?? null,
144
+ filterNamespace: "global",
145
+ });
146
+ }
147
+ if (toUpsert.length > 0 || toDelete.length > 0) {
148
+ refreshScreen(table);
149
+ }
150
+ log(`[Supastash] Batch received ${rows.length} rows for "${table}" ` +
151
+ `(u${toUpsert.length}/d${toDelete.length})`);
152
+ SyncInfoUpdater.markLogSuccess({ type: "pull", table });
153
+ }
154
+ catch (e) {
155
+ SyncInfoUpdater.markLogError({
156
+ type: "pull",
157
+ table,
158
+ lastError: e,
159
+ errorCount: 1,
160
+ });
161
+ logError(`[Supastash] Batch pull failed for "${table}"`, e);
162
+ }
163
+ finally {
164
+ // Mark table as fully completed only once it leaves remaining_tables
165
+ if (!nextRemaining.includes(table)) {
166
+ completedTables.add(table);
167
+ SyncInfoUpdater.setTablesCompleted({
168
+ amount: completedTables.size,
169
+ type: "pull",
170
+ });
171
+ }
172
+ }
173
+ }
174
+ remainingTables = nextRemaining;
175
+ }
176
+ }
177
+ catch (error) {
178
+ logError("[Supastash] Error in batch pull from remote", error);
179
+ }
180
+ finally {
181
+ SyncInfoUpdater.reset({ type: "pull" });
182
+ }
183
+ }
@@ -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;AAmMnE;;;;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
+ {"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
- attempts++;
92
- await backoff(attempts);
93
- pending = [...preflightOK];
94
- continue;
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
- // Backoff before next batch round (exponential, bounded by policy)
141
- attempts++;
142
- await backoff(attempts);
143
- pending = keep;
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({
@@ -2,7 +2,7 @@ import { SupastashConfig } from "../../../../shared/types/supastashConfig.types"
2
2
  import { RowLike } from "../../../../shared/types/syncEngine.types";
3
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,wFAYvB;AAED,iBAAe,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,gBAEvE;AAED,iBAAe,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,gBAErE;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"}
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"}
@@ -26,8 +26,30 @@ export function classifyFailure(cfg, code) {
26
26
  async function batchUpsert(table, rows, supabase) {
27
27
  return await supabase.from(table).upsert(rows);
28
28
  }
29
- async function singleUpsert(table, row, supabase) {
30
- return await supabase.from(table).upsert(row).select("id").maybeSingle();
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();
31
53
  }
32
54
  async function backoff(attempts) {
33
55
  const config = getSupastashConfig();
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shared/core/config/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AAmC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,0BAA0B,EACrE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,gBAAgB,EAAE,CAAC,CAAA;CAAE,QAwCrD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,CAAC,SAAS,0BAA0B,KACjC,eAAe,CAAC,CAAC,CAAC,CAEtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CACnC,CAAC,SAAS,0BAA0B,EACpC,MAAM,EAAE;IACR,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;QAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;KAC3B,CAAC;CACH,QAEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shared/core/config/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AAqC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,0BAA0B,EACrE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,gBAAgB,EAAE,CAAC,CAAA;CAAE,QAwCrD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,CAAC,SAAS,0BAA0B,KACjC,eAAe,CAAC,CAAC,CAAC,CAEtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CACnC,CAAC,SAAS,0BAA0B,EACpC,MAAM,EAAE;IACR,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;QAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;KAC3B,CAAC;CACH,QAEA"}
@@ -16,6 +16,8 @@ let _config = {
16
16
  },
17
17
  platform: "native",
18
18
  replicationMode: "client-side",
19
+ useBatchPullSync: false,
20
+ useBatchSchemaFetch: false,
19
21
  hasEnabledSimpleNullHandling: false,
20
22
  maxSyncLookbackDays: 365,
21
23
  perTableSyncLookbackDays: undefined,
@@ -1,4 +1,5 @@
1
1
  import { SupastashFilter } from "../../types/realtimeData.types";
2
+ import { RpcTableFilters } from "../../types/rpcFilter.types";
2
3
  /**
3
4
  * useSupastashFilters
4
5
  *
@@ -26,11 +27,15 @@ import { SupastashFilter } from "../../types/realtimeData.types";
26
27
  * });
27
28
  * ```
28
29
  *
29
- * @param {SupastashFilter} filters - An object where each key is a table name, and its value is
30
- * an array of `SupastashFilter` objects that define the filter criteria for that table's pull sync.
30
+ * @param {Record<string, SupastashFilter[]>} filters - Per-table filters applied to both the
31
+ * per-table pull path and (automatically converted) the batch RPC pull path.
32
+ * Covers eq, neq, gt, gte, lt, lte, in, is (null / not-null), and or-groups.
33
+ * @param {RpcTableFilters} rpcFilters - Optional supplemental RPC filter nodes for the batch
34
+ * pull path only. Only needed when you require `and` groups, which `SupastashFilter` doesn't
35
+ * support. These are merged with the auto-converted `filters` before the RPC call.
31
36
  *
32
- * @note This hook does not re-run unless the `filters` object reference changes.
37
+ * @note This hook does not re-run unless the `filters` or `rpcFilters` object reference changes.
33
38
  * To force re-evaluation, pass a fresh object (not just mutated data).
34
39
  */
35
- export declare function useSupastashFilters(filters?: Record<string, SupastashFilter[]>): void;
40
+ export declare function useSupastashFilters(filters?: Record<string, SupastashFilter[]>, rpcFilters?: RpcTableFilters): void;
36
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shared/hooks/supastashFilters/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAMjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,QAyD5C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shared/hooks/supastashFilters/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAO9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,EAC3C,UAAU,CAAC,EAAE,eAAe,QA6D7B"}
@@ -2,6 +2,7 @@ import { useEffect } from "react";
2
2
  import { filterTracker, tableFilters, tableFiltersUsed, } from "../../store/tableFilters";
3
3
  import { logWarn } from "../../utils/logs";
4
4
  import { ReusedHelpers } from "../../utils/reusedHelpers";
5
+ import { updateRpcFilters } from "../../utils/sync/pullFromRemote/updateRpcFilters";
5
6
  import { warnOnMisMatch } from "../../utils/sync/pullFromRemote/validateFilters";
6
7
  import { checkIfTableExist } from "../../utils/tableValidator";
7
8
  /**
@@ -31,16 +32,23 @@ import { checkIfTableExist } from "../../utils/tableValidator";
31
32
  * });
32
33
  * ```
33
34
  *
34
- * @param {SupastashFilter} filters - An object where each key is a table name, and its value is
35
- * an array of `SupastashFilter` objects that define the filter criteria for that table's pull sync.
35
+ * @param {Record<string, SupastashFilter[]>} filters - Per-table filters applied to both the
36
+ * per-table pull path and (automatically converted) the batch RPC pull path.
37
+ * Covers eq, neq, gt, gte, lt, lte, in, is (null / not-null), and or-groups.
38
+ * @param {RpcTableFilters} rpcFilters - Optional supplemental RPC filter nodes for the batch
39
+ * pull path only. Only needed when you require `and` groups, which `SupastashFilter` doesn't
40
+ * support. These are merged with the auto-converted `filters` before the RPC call.
36
41
  *
37
- * @note This hook does not re-run unless the `filters` object reference changes.
42
+ * @note This hook does not re-run unless the `filters` or `rpcFilters` object reference changes.
38
43
  * To force re-evaluation, pass a fresh object (not just mutated data).
39
44
  */
40
- export function useSupastashFilters(filters) {
45
+ export function useSupastashFilters(filters, rpcFilters) {
41
46
  useEffect(() => {
42
47
  let cancelled = false;
43
48
  async function run() {
49
+ if (rpcFilters) {
50
+ await updateRpcFilters(rpcFilters);
51
+ }
44
52
  if (!filters)
45
53
  return;
46
54
  const incoming = Object.keys(filters);
@@ -82,5 +90,5 @@ export function useSupastashFilters(filters) {
82
90
  return () => {
83
91
  cancelled = true;
84
92
  };
85
- }, [filters]);
93
+ }, [filters, rpcFilters]);
86
94
  }
@@ -0,0 +1,7 @@
1
+ import { RpcFilterNode } from "../types/rpcFilter.types";
2
+ /**
3
+ * Stores per-table RPC filter nodes used by the batch pull sync path.
4
+ * Keyed by table name, value is the array of RpcFilterNode to apply.
5
+ */
6
+ export declare const rpcTableFilters: Map<string, RpcFilterNode[]>;
7
+ //# sourceMappingURL=rpcTableFilters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpcTableFilters.d.ts","sourceRoot":"","sources":["../../../src/shared/store/rpcTableFilters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,eAAe,8BAAqC,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Stores per-table RPC filter nodes used by the batch pull sync path.
3
+ * Keyed by table name, value is the array of RpcFilterNode to apply.
4
+ */
5
+ export const rpcTableFilters = new Map();
@@ -0,0 +1,23 @@
1
+ export type RpcFilterOp =
2
+ | "eq"
3
+ | "neq"
4
+ | "gt"
5
+ | "gte"
6
+ | "lt"
7
+ | "lte"
8
+ | "in"
9
+ | "is_null"
10
+ | "is_not_null";
11
+
12
+ export type RpcSimpleFilter = {
13
+ col: string;
14
+ op: RpcFilterOp;
15
+ val?: string | number;
16
+ };
17
+
18
+ export type RpcFilterNode =
19
+ | RpcSimpleFilter
20
+ | { or: RpcFilterNode[] }
21
+ | { and: RpcFilterNode[] };
22
+
23
+ export type RpcTableFilters = Record<string, RpcFilterNode[]>;
@@ -89,12 +89,12 @@ export type SupastashConfig<T extends SupastashSQLiteClientTypes> = {
89
89
  sqliteClient: T extends "expo"
90
90
  ? ExpoSQLiteClient
91
91
  : T extends "rn-storage"
92
- ? RNStorageSQLiteClient
93
- : T extends "rn-nitro"
94
- ? RNSqliteNitroClient
95
- : T extends "tauri"
96
- ? TauriSQLiteClient
97
- : null;
92
+ ? RNStorageSQLiteClient
93
+ : T extends "rn-nitro"
94
+ ? RNSqliteNitroClient
95
+ : T extends "tauri"
96
+ ? TauriSQLiteClient
97
+ : null;
98
98
 
99
99
  /**
100
100
  * Runtime platform.
@@ -319,6 +319,34 @@ export type SupastashConfig<T extends SupastashSQLiteClientTypes> = {
319
319
  * Supastash docs: https://0xzekea.github.io/supastash/docs/sync-calls#%EF%B8%8F-pushrpcpath-custom-batch-sync-rpc
320
320
  */
321
321
  pushRPCPath?: string;
322
+ /**
323
+ * When true, pull sync uses a single `supastash_pull_sync` RPC call to fetch
324
+ * all tables in one round trip instead of per-table queries.
325
+ *
326
+ * The RPC handles pagination internally via `remaining_tables` — Supastash
327
+ * will keep calling until all tables are fully synced.
328
+ *
329
+ * Use `updateRpcFilters` to set per-table filters for this mode.
330
+ *
331
+ * ⚠️ Requires the `supastash_pull_sync` Postgres function to be deployed
332
+ * and RLS to be enabled on every table you expose.
333
+ *
334
+ * @default false
335
+ */
336
+ useBatchPullSync?: boolean;
337
+ /**
338
+ * When true, Supastash fetches column metadata for all tables in a single
339
+ * `get_table_schemas` RPC call at the start of each sync cycle instead of
340
+ * calling `get_table_schema` once per table on demand.
341
+ *
342
+ * Warms the in-memory and SQLite schema caches up front so every subsequent
343
+ * per-table lookup is served from cache with zero extra network calls.
344
+ *
345
+ * Requires the `get_table_schemas` Postgres function to be deployed.
346
+ *
347
+ * @default false
348
+ */
349
+ useBatchSchemaFetch?: boolean;
322
350
  /**
323
351
  * Controls how Supastash operates at runtime.
324
352
  *
@@ -443,7 +471,7 @@ export interface SupastashSQLiteExecutor {
443
471
  */
444
472
  queryOne<T = any>(
445
473
  sql: string,
446
- params?: Record<string, any>
474
+ params?: Record<string, any>,
447
475
  ): Promise<T | null>;
448
476
  /**
449
477
  * Executes a write or mutation statement.
@@ -500,14 +528,14 @@ export interface SupastashSQLiteDatabase extends SupastashSQLiteExecutor {
500
528
  * @returns A Promise that resolves when the transaction is complete
501
529
  */
502
530
  withTransaction(
503
- fn: (tx: SupastashSQLiteExecutor) => Promise<void> | void
531
+ fn: (tx: SupastashSQLiteExecutor) => Promise<void> | void,
504
532
  ): Promise<void>;
505
533
  }
506
534
 
507
535
  export interface SupastashSQLiteAdapter<TClient = any> {
508
536
  openDatabaseAsync(
509
537
  name: string,
510
- sqliteClient: TClient
538
+ sqliteClient: TClient,
511
539
  ): Promise<SupastashSQLiteDatabase>;
512
540
  }
513
541
 
@@ -515,7 +543,7 @@ export interface ExpoSQLiteClient {
515
543
  openDatabaseAsync: (
516
544
  databaseName: string,
517
545
  options?: SQLiteOpenOptions | undefined,
518
- directory?: string | undefined
546
+ directory?: string | undefined,
519
547
  ) => Promise<ExpoSQLiteDatabase>;
520
548
  }
521
549
 
@@ -1 +1 @@
1
- {"version":3,"file":"createSyncStatus.d.ts","sourceRoot":"","sources":["../../../../src/shared/utils/schema/createSyncStatus.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAsB,wBAAwB,kBAE7C;AAED,eAAO,MAAM,sBAAsB,0ZAUhC,CAAC;AAEJ,eAAO,MAAM,6BAA6B,6eAezC,CAAC;AAEF,eAAO,MAAM,wBAAwB,mFAEpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,2GAGhC,CAAC;AAEF,eAAO,MAAM,2BAA2B,yHAGvC,CAAC;AAIF;;;;GAIG;AACH,wBAAsB,qBAAqB,kBAuB1C;AAED,wEAAwE;AACxE,wBAAgB,yBAAyB,SAExC"}
1
+ {"version":3,"file":"createSyncStatus.d.ts","sourceRoot":"","sources":["../../../../src/shared/utils/schema/createSyncStatus.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAsB,wBAAwB,kBAE7C;AAED,eAAO,MAAM,sBAAsB,0ZAUhC,CAAC;AAEJ,eAAO,MAAM,6BAA6B,6eAezC,CAAC;AAEF,eAAO,MAAM,wBAAwB,mFAEpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,2GAGhC,CAAC;AAEF,eAAO,MAAM,2BAA2B,yHAGvC,CAAC;AAIF;;;;GAIG;AACH,wBAAsB,qBAAqB,kBAiC1C;AAED,wEAAwE;AACxE,wBAAgB,yBAAyB,SAExC"}
@@ -68,7 +68,11 @@ export async function createSyncStatusTable() {
68
68
  await db.execAsync(INDEX_SYNC_MARKS_SQL);
69
69
  }
70
70
  try {
71
- await db.execAsync(ADD_PK_TO_SYNC_MARKS_SQL);
71
+ const columns = await db.getAllAsync(`SELECT name FROM pragma_table_info('supastash_sync_marks')`);
72
+ const hasColumn = columns.some((column) => column.name === "last_synced_at_pk");
73
+ if (!hasColumn) {
74
+ await db.execAsync(ADD_PK_TO_SYNC_MARKS_SQL);
75
+ }
72
76
  }
73
77
  catch {
74
78
  // Ignore — column already exists
@@ -0,0 +1,9 @@
1
+ import { SupastashFilter } from "../../../types/realtimeData.types";
2
+ import { RpcFilterNode } from "../../../types/rpcFilter.types";
3
+ /**
4
+ * Converts PostgREST-style SupastashFilter[] to RpcFilterNode[].
5
+ * Supports simple filters and { or: [...] } groups.
6
+ * Returns an empty array for an empty/undefined input.
7
+ */
8
+ export declare function postgrestFiltersToRpc(filters: SupastashFilter[] | undefined): RpcFilterNode[];
9
+ //# sourceMappingURL=postgrestToRpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgrestToRpc.d.ts","sourceRoot":"","sources":["../../../../../src/shared/utils/sync/pullFromRemote/postgrestToRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAe,MAAM,gCAAgC,CAAC;AA+B5E;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,GACrC,aAAa,EAAE,CAgBjB"}