supastash 0.2.15 → 0.2.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/desktop/hooks/syncEngine.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AA2DD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA2D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/desktop/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA2D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
@@ -9,6 +9,7 @@ import { subscribeToAppVisibility } from "../adapters/appstate";
9
9
  import { pullFromRemote as doPullFromRemote } from "../utils/sync/pullFromRemote";
10
10
  import { pullFromRemoteBatch } from "../utils/sync/pullFromRemote/pullFromRemoteBatch";
11
11
  import { updateLocalDb } from "../utils/sync/pullFromRemote/updateLocalDb";
12
+ import { pushLocalData as doPushLocalData } from "../utils/sync/pushLocal";
12
13
  import { pushLocalDataToRemote } from "../utils/sync/pushLocal/sendUnsyncedToSupabase";
13
14
  // -----------------------------
14
15
  // Module-scoped state & tunables
@@ -85,12 +86,7 @@ async function pushLocalDataSafe() {
85
86
  return;
86
87
  isPushing = true;
87
88
  try {
88
- if (cfg.useBatchPullSync) {
89
- await pullFromRemoteBatch();
90
- }
91
- else {
92
- await doPullFromRemote();
93
- }
89
+ await doPushLocalData();
94
90
  lastPushAt = Date.now();
95
91
  }
96
92
  catch (e) {
@@ -108,7 +104,6 @@ async function pullFromRemoteSafe() {
108
104
  return;
109
105
  if (!(await isOnline()))
110
106
  return;
111
- // If in ghost mode, don't pull
112
107
  const cfg = getSupastashConfig();
113
108
  if (cfg.supastashMode === "ghost")
114
109
  return;
@@ -116,7 +111,12 @@ async function pullFromRemoteSafe() {
116
111
  return;
117
112
  isPulling = true;
118
113
  try {
119
- await doPullFromRemote();
114
+ if (cfg.useBatchPullSync) {
115
+ await pullFromRemoteBatch();
116
+ }
117
+ else {
118
+ await doPullFromRemote();
119
+ }
120
120
  lastPullAt = Date.now();
121
121
  }
122
122
  catch (e) {
@@ -206,7 +206,12 @@ export async function syncTable(table) {
206
206
  // Pull
207
207
  if (cfg.syncEngine?.pull) {
208
208
  try {
209
- await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
209
+ if (cfg.useBatchPullSync) {
210
+ await pullFromRemoteBatch([table]);
211
+ }
212
+ else {
213
+ await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
214
+ }
210
215
  }
211
216
  catch (e) {
212
217
  log("[Supastash] syncTable pull error", {
@@ -5,5 +5,5 @@
5
5
  * Requires `useBatchPullSync: true` in config and the
6
6
  * `supastash_pull_sync` Postgres function to be deployed.
7
7
  */
8
- export declare function pullFromRemoteBatch(): Promise<void>;
8
+ export declare function pullFromRemoteBatch(specificTables?: string[]): Promise<void>;
9
9
  //# sourceMappingURL=pullFromRemoteBatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4MzD"}
1
+ {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Mf"}
@@ -37,14 +37,14 @@ function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
37
37
  * Requires `useBatchPullSync: true` in config and the
38
38
  * `supastash_pull_sync` Postgres function to be deployed.
39
39
  */
40
- export async function pullFromRemoteBatch() {
40
+ export async function pullFromRemoteBatch(specificTables) {
41
41
  const cfg = getSupastashConfig();
42
42
  const supabase = cfg.supabaseClient;
43
43
  if (!supabase)
44
44
  throw new Error(`No supabase client found: ${supabaseClientErr}`);
45
45
  if (cfg.supastashMode === "ghost")
46
46
  return;
47
- const tables = await getAllTables();
47
+ const tables = specificTables ?? (await getAllTables());
48
48
  if (!tables) {
49
49
  log("[Supastash] Batch pull: no tables found");
50
50
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/native/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA+D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/native/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA+D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
@@ -208,7 +208,12 @@ export async function syncTable(table) {
208
208
  // Pull
209
209
  if (cfg.syncEngine?.pull) {
210
210
  try {
211
- await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
211
+ if (cfg.useBatchPullSync) {
212
+ await pullFromRemoteBatch([table]);
213
+ }
214
+ else {
215
+ await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
216
+ }
212
217
  }
213
218
  catch (e) {
214
219
  log("[Supastash] syncTable pull error", {
@@ -5,5 +5,5 @@
5
5
  * Requires `useBatchPullSync: true` in config and the
6
6
  * `supastash_pull_sync` Postgres function to be deployed.
7
7
  */
8
- export declare function pullFromRemoteBatch(): Promise<void>;
8
+ export declare function pullFromRemoteBatch(specificTables?: string[]): Promise<void>;
9
9
  //# sourceMappingURL=pullFromRemoteBatch.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Mf"}
@@ -2,16 +2,16 @@ import { getSupastashConfig } from "../../../../shared/core/config";
2
2
  import { getSupastashDb } from "../../../../shared/db/dbInitializer";
3
3
  import { rpcTableFilters } from "../../../../shared/store/rpcTableFilters";
4
4
  import { tableFilters } from "../../../../shared/store/tableFilters";
5
- import { postgrestFiltersToRpc } from "../../../../shared/utils/sync/pullFromRemote/postgrestToRpc";
5
+ import log, { logError, logWarn } from "../../../../shared/utils/logs";
6
+ import { refreshScreen } from "../../../../shared/utils/refreshScreenCalls";
7
+ import { supabaseClientErr } from "../../../../shared/utils/supabaseClientErr";
6
8
  import { getAllTables } from "../../../../shared/utils/sync/getAllTables";
7
9
  import { getMaxSyncLookBack, logNoUpdates, returnMaxDate, } from "../../../../shared/utils/sync/pullFromRemote/helpers";
10
+ import { postgrestFiltersToRpc } from "../../../../shared/utils/sync/pullFromRemote/postgrestToRpc";
8
11
  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
12
  import { prefetchRemoteTableSchemas } from "../../../../shared/utils/sync/status/remoteSchema";
13
- import { setSupastashSyncStatus } from "../status/services";
14
13
  import { selectSyncStatus } from "../status/repo";
14
+ import { setSupastashSyncStatus } from "../status/services";
15
15
  import { upsertChunkData } from "./updateLocalDb";
16
16
  const CHUNK_SIZE = 999;
17
17
  function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
@@ -37,14 +37,14 @@ function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
37
37
  * Requires `useBatchPullSync: true` in config and the
38
38
  * `supastash_pull_sync` Postgres function to be deployed.
39
39
  */
40
- export async function pullFromRemoteBatch() {
40
+ export async function pullFromRemoteBatch(specificTables) {
41
41
  const cfg = getSupastashConfig();
42
42
  const supabase = cfg.supabaseClient;
43
43
  if (!supabase)
44
44
  throw new Error(`No supabase client found: ${supabaseClientErr}`);
45
45
  if (cfg.supastashMode === "ghost")
46
46
  return;
47
- const tables = await getAllTables();
47
+ const tables = specificTables ?? (await getAllTables());
48
48
  if (!tables) {
49
49
  log("[Supastash] Batch pull: no tables found");
50
50
  return;
@@ -57,7 +57,10 @@ export async function pullFromRemoteBatch() {
57
57
  const db = await getSupastashDb();
58
58
  const completedTables = new Set();
59
59
  SyncInfoUpdater.setInProgress({ action: "start", type: "pull" });
60
- SyncInfoUpdater.setNumberOfTables({ amount: tablesToPull.length, type: "pull" });
60
+ SyncInfoUpdater.setNumberOfTables({
61
+ amount: tablesToPull.length,
62
+ type: "pull",
63
+ });
61
64
  // Warm schema cache for all tables in one call if enabled
62
65
  if (cfg.useBatchSchemaFetch) {
63
66
  await prefetchRemoteTableSchemas(tablesToPull);
@@ -112,8 +115,16 @@ export async function pullFromRemoteBatch() {
112
115
  logWarn(`[Supastash] Batch: skipped row without id from "${table}"`);
113
116
  continue;
114
117
  }
115
- prevMaxSyncedAt = returnMaxDate({ row, prevMax: prevMaxSyncedAt, col: tsCol });
116
- prevMaxDeletedAt = returnMaxDate({ row, prevMax: prevMaxDeletedAt, col: "deleted_at" });
118
+ prevMaxSyncedAt = returnMaxDate({
119
+ row,
120
+ prevMax: prevMaxSyncedAt,
121
+ col: tsCol,
122
+ });
123
+ prevMaxDeletedAt = returnMaxDate({
124
+ row,
125
+ prevMax: prevMaxDeletedAt,
126
+ col: "deleted_at",
127
+ });
117
128
  if (row.deleted_at) {
118
129
  toDelete.push(row.id);
119
130
  }
@@ -121,8 +132,16 @@ export async function pullFromRemoteBatch() {
121
132
  toUpsert.push(row);
122
133
  }
123
134
  }
124
- SyncInfoUpdater.setUnsyncedDataCount({ amount: toUpsert.length, type: "pull", table });
125
- SyncInfoUpdater.setUnsyncedDeletedCount({ amount: toDelete.length, type: "pull", table });
135
+ SyncInfoUpdater.setUnsyncedDataCount({
136
+ amount: toUpsert.length,
137
+ type: "pull",
138
+ table,
139
+ });
140
+ SyncInfoUpdater.setUnsyncedDeletedCount({
141
+ amount: toDelete.length,
142
+ type: "pull",
143
+ table,
144
+ });
126
145
  // Delete soft-deleted rows
127
146
  if (toDelete.length > 0) {
128
147
  for (let i = 0; i < toDelete.length; i += CHUNK_SIZE) {
@@ -1,4 +1,13 @@
1
1
  import { PayloadData } from "../../../types/query.types";
2
+ import { SupastashFilter } from "../../../types/realtimeData.types";
3
+ /**
4
+ * Builds a combined " AND (...)" SQL string + params array from a list of filters.
5
+ * Never throws — malformed filters are skipped and logged.
6
+ */
7
+ export declare function buildFilterSql(filters: SupastashFilter[]): {
8
+ sql: string;
9
+ params: unknown[];
10
+ };
2
11
  /**
3
12
  * Gets all unsynced data from a table
4
13
  * @param table - The table to get the data from
@@ -1 +1 @@
1
- {"version":3,"file":"getAllUnsyncedData.d.ts","sourceRoot":"","sources":["../../../../../src/shared/utils/sync/pushLocal/getAllUnsyncedData.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAwCzD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAe/B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAM/B"}
1
+ {"version":3,"file":"getAllUnsyncedData.d.ts","sourceRoot":"","sources":["../../../../../src/shared/utils/sync/pushLocal/getAllUnsyncedData.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAgGpE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB,CAYA;AAmCD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAuB/B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAS/B"}
@@ -1,8 +1,99 @@
1
1
  import { getSupastashDb } from "../../../db/dbInitializer";
2
+ import { tableFilters } from "../../../store/tableFilters";
2
3
  import { getTableSchema } from "../../getTableSchema";
3
4
  import log from "../../logs";
4
5
  import { getRemoteTableSchema } from "../status/remoteSchema";
5
6
  const sharedKeysCache = new Map();
7
+ const VALID_OPERATORS = new Set([
8
+ "eq",
9
+ "neq",
10
+ "gt",
11
+ "gte",
12
+ "lt",
13
+ "lte",
14
+ "is",
15
+ "in",
16
+ ]);
17
+ function escapeColumn(column) {
18
+ // Basic guard against injection via column names (should only ever be internal identifiers)
19
+ return `"${column.replace(/"/g, '""')}"`;
20
+ }
21
+ /**
22
+ * Builds a single SQL clause + params for one filter condition.
23
+ * Returns null (instead of throwing) if the filter is malformed.
24
+ */
25
+ function buildSingleClause(f) {
26
+ try {
27
+ // Composite OR filter: { or: SupastashFilter[] }
28
+ if ("or" in f && Array.isArray(f.or)) {
29
+ const orFilters = f.or;
30
+ const built = orFilters
31
+ .map((sub) => buildSingleClause(sub))
32
+ .filter((c) => !!c);
33
+ if (!built.length)
34
+ return null;
35
+ const sql = "(" + built.map((c) => c.sql).join(" OR ") + ")";
36
+ const params = built.flatMap((c) => c.params);
37
+ return { sql, params };
38
+ }
39
+ // Flat filter: { column, operator, value }
40
+ const { column, operator, value } = f;
41
+ if (!column || !operator || !VALID_OPERATORS.has(operator)) {
42
+ log(`[Supastash] Skipping invalid filter: ${JSON.stringify(f)}`);
43
+ return null;
44
+ }
45
+ const col = escapeColumn(column);
46
+ switch (operator) {
47
+ case "is":
48
+ // sqlite uses IS for null comparisons; also support IS TRUE/FALSE-ish via 0/1
49
+ if (value === null) {
50
+ return { sql: `${col} IS NULL`, params: [] };
51
+ }
52
+ return { sql: `${col} IS ?`, params: [value] };
53
+ case "in": {
54
+ const values = Array.isArray(value) ? value : [value];
55
+ if (!values.length)
56
+ return null;
57
+ const placeholders = values.map(() => "?").join(", ");
58
+ return { sql: `${col} IN (${placeholders})`, params: values };
59
+ }
60
+ case "eq":
61
+ return { sql: `${col} = ?`, params: [value] };
62
+ case "neq":
63
+ return { sql: `${col} != ?`, params: [value] };
64
+ case "gt":
65
+ return { sql: `${col} > ?`, params: [value] };
66
+ case "gte":
67
+ return { sql: `${col} >= ?`, params: [value] };
68
+ case "lt":
69
+ return { sql: `${col} < ?`, params: [value] };
70
+ case "lte":
71
+ return { sql: `${col} <= ?`, params: [value] };
72
+ default:
73
+ return null;
74
+ }
75
+ }
76
+ catch (err) {
77
+ log(`[Supastash] Failed to build filter clause: ${String(err)}`);
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Builds a combined " AND (...)" SQL string + params array from a list of filters.
83
+ * Never throws — malformed filters are skipped and logged.
84
+ */
85
+ export function buildFilterSql(filters) {
86
+ if (!filters?.length)
87
+ return { sql: "", params: [] };
88
+ const clauses = filters
89
+ .map((f) => buildSingleClause(f))
90
+ .filter((c) => !!c);
91
+ if (!clauses.length)
92
+ return { sql: "", params: [] };
93
+ const sql = " AND " + clauses.map((c) => c.sql).join(" AND ");
94
+ const params = clauses.flatMap((c) => c.params);
95
+ return { sql, params };
96
+ }
6
97
  async function getRemoteKeys(table) {
7
98
  if (sharedKeysCache.has(table)) {
8
99
  return sharedKeysCache.get(table);
@@ -30,15 +121,17 @@ async function getRemoteKeys(table) {
30
121
  */
31
122
  export async function getAllUnsyncedData(table) {
32
123
  const db = await getSupastashDb();
124
+ const filters = tableFilters.get(table) ?? [];
125
+ const filterSqlDetails = buildFilterSql(filters);
33
126
  const remoteKeys = await getRemoteKeys(table);
34
127
  if (!remoteKeys) {
35
- const query = `SELECT * FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL`;
36
- const data = await db.getAllAsync(query);
128
+ const query = `SELECT * FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL${filterSqlDetails.sql}`;
129
+ const data = await db.getAllAsync(query, filterSqlDetails.params);
37
130
  return data ?? null;
38
131
  }
39
132
  const columns = remoteKeys.join(", ");
40
- const query = `SELECT ${columns} FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL`;
41
- const data = await db.getAllAsync(query);
133
+ const query = `SELECT ${columns} FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL${filterSqlDetails.sql}`;
134
+ const data = await db.getAllAsync(query, filterSqlDetails.params);
42
135
  return data ?? null;
43
136
  }
44
137
  /**
@@ -48,6 +141,8 @@ export async function getAllUnsyncedData(table) {
48
141
  */
49
142
  export async function getAllDeletedData(table) {
50
143
  const db = await getSupastashDb();
51
- const data = await db.getAllAsync(`SELECT deleted_at, id FROM ${table} WHERE deleted_at IS NOT NULL`);
144
+ const filters = tableFilters.get(table) ?? [];
145
+ const filterSqlDetails = buildFilterSql(filters);
146
+ const data = await db.getAllAsync(`SELECT deleted_at, id FROM ${table} WHERE deleted_at IS NOT NULL${filterSqlDetails.sql}`, filterSqlDetails.params);
52
147
  return data ?? null;
53
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supastash",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",