supastash 0.1.19 → 0.1.20

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.
@@ -5,6 +5,7 @@ export declare function assignInsertIds<R>(payload: R | R[] | null): R | R[] | n
5
5
  export declare function getCommonError<U extends boolean, T extends CrudMethods, R, Z>(table: string, method: CrudMethods, localResult: MethodReturnTypeMap<U, Z>[T] | null, remoteResult: SupabaseQueryReturn<U, Z> | null): (Error & {
6
6
  supabaseError?: PostgrestError;
7
7
  }) | null;
8
+ export declare function addToCache(state: SupastashQuery<CrudMethods, boolean, any>): void;
8
9
  export declare function runSyncStrategy<T extends CrudMethods, U extends boolean, R, Z>(state: SupastashQuery<T, U, R>): Promise<{
9
10
  localResult: MethodReturnTypeMap<U, Z>[T] | null;
10
11
  remoteResult: SupabaseQueryReturn<U, Z> | null;
@@ -1 +1 @@
1
- {"version":3,"file":"mainQueryHelpers.d.ts","sourceRoot":"","sources":["../../../../src/utils/query/helpers/mainQueryHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACf,MAAM,4BAA4B,CAAC;AAOpC,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,IAAI,CAUN;AAED,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GACtB,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,CAc5B;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,CAAC,EAC3E,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAChD,YAAY,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,GAC7C,CAAC,KAAK,GAAG;IAAE,aAAa,CAAC,EAAE,cAAc,CAAA;CAAE,CAAC,GAAG,IAAI,CAwBrD;AA4GD,wBAAsB,eAAe,CACnC,CAAC,SAAS,WAAW,EACrB,CAAC,SAAS,OAAO,EACjB,CAAC,EACD,CAAC,EAED,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAC7B,OAAO,CAAC;IACT,WAAW,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACjD,YAAY,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;CAChD,CAAC,CA2CD"}
1
+ {"version":3,"file":"mainQueryHelpers.d.ts","sourceRoot":"","sources":["../../../../src/utils/query/helpers/mainQueryHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACf,MAAM,4BAA4B,CAAC;AAOpC,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,IAAI,CAUN;AAED,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GACtB,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,CAc5B;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,CAAC,EAC3E,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAChD,YAAY,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,GAC7C,CAAC,KAAK,GAAG;IAAE,aAAa,CAAC,EAAE,cAAc,CAAA;CAAE,CAAC,GAAG,IAAI,CAwBrD;AAiBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,QAQ1E;AAmDD,wBAAsB,eAAe,CACnC,CAAC,SAAS,WAAW,EACrB,CAAC,SAAS,OAAO,EACjB,CAAC,EACD,CAAC,EAED,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAC7B,OAAO,CAAC;IACT,WAAW,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACjD,YAAY,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;CAChD,CAAC,CA2CD"}
@@ -1,5 +1,5 @@
1
1
  import { isOnline } from "../../../utils/connection";
2
- import log, { logError } from "../../../utils/logs";
2
+ import { log, logError } from "../../../utils/logs";
3
3
  import { generateUUIDv4 } from "../../genUUID";
4
4
  import { queryLocalDb } from "../localDbQuery";
5
5
  import { querySupabase } from "../remoteQuery/supabaseQuery";
@@ -46,85 +46,62 @@ export function getCommonError(table, method, localResult, remoteResult) {
46
46
  }
47
47
  return null;
48
48
  }
49
- let stateCache = [];
50
- const pendingIds = new Set();
51
- let isRunning = false;
49
+ const stateCache = new Map();
50
+ const runningStates = new Set();
51
+ const calledOfflineRetries = new Map();
52
+ const retryCount = new Map();
52
53
  const MAX_RETRIES = 2;
53
54
  const MAX_OFFLINE_RETRIES = 10;
54
- const calledOfflineRetries = new Map();
55
- const retryDelay = 1000 * 30;
56
- function enqueueState(query) {
57
- if (pendingIds.has(query.id))
55
+ const getOpKey = (state) => `${state.method}:${state.table}:${state.id}`;
56
+ function delay(ms) {
57
+ return new Promise((res) => setTimeout(res, ms));
58
+ }
59
+ export function addToCache(state) {
60
+ const opKey = getOpKey(state);
61
+ if (runningStates.has(opKey))
58
62
  return;
59
- stateCache.push(query);
60
- pendingIds.add(query.id);
63
+ runningStates.add(opKey);
64
+ stateCache.set(opKey, state);
65
+ runRemoteQuery(opKey);
61
66
  }
62
- async function runBatchedRemoteQuery() {
63
- if (isRunning)
67
+ async function runRemoteQuery(opKey) {
68
+ const state = stateCache.get(opKey);
69
+ if (!state)
64
70
  return;
65
- isRunning = true;
66
- while (stateCache.length > 0) {
67
- const isConnected = await isOnline();
68
- const state = stateCache.shift();
69
- if (calledOfflineRetries.get(state.id) &&
70
- calledOfflineRetries.get(state.id) >= MAX_OFFLINE_RETRIES) {
71
- logError(`[Supastash] Failed to send remote batch query:\n` +
72
- ` Table: ${state.table}\n` +
73
- ` Method: ${state.method}\n` +
74
- ` Retries: ${MAX_OFFLINE_RETRIES}\n` +
75
- ` Retrying in ${retryDelay}ms`);
76
- calledOfflineRetries.delete(state.id);
77
- batchTimer = setTimeout(() => {
78
- isRunning = false;
79
- if (!isRunning)
80
- runBatchedRemoteQuery();
81
- batchTimer = null;
82
- }, retryDelay);
83
- return;
84
- }
85
- if (!isConnected) {
86
- if (!calledOfflineRetries.has(state.id)) {
87
- log(`[Supastash] Not connected to internet\n` +
88
- ` Table: ${state.table}\n` +
89
- ` Method: ${state.method}\n` +
90
- ` Retries: ${MAX_OFFLINE_RETRIES}\n`);
91
- }
92
- stateCache.unshift(state);
93
- calledOfflineRetries.set(state.id, (calledOfflineRetries.get(state.id) || 0) + 1);
94
- await delay(1000);
95
- continue;
96
- }
97
- calledOfflineRetries.delete(state.id);
98
- const retryCount = state.retryCount || 0;
99
- const { error } = await querySupabase({
100
- ...state,
101
- isSingle: state.isSingle,
102
- }, true);
103
- if (error) {
104
- if (retryCount < MAX_RETRIES) {
105
- state.retryCount = retryCount + 1;
106
- stateCache.unshift(state);
107
- await delay(100 * retryCount);
71
+ retryCount.set(opKey, retryCount.get(opKey) ?? 0);
72
+ try {
73
+ while ((retryCount.get(opKey) ?? 0) <= MAX_RETRIES) {
74
+ const isConnected = await isOnline();
75
+ if (!isConnected) {
76
+ const offlineRetries = (calledOfflineRetries.get(opKey) || 0) + 1;
77
+ if (offlineRetries > MAX_OFFLINE_RETRIES) {
78
+ logError(`[Supastash] Gave up on ${opKey} after ${MAX_OFFLINE_RETRIES} offline retries`);
79
+ break;
80
+ }
81
+ calledOfflineRetries.set(opKey, offlineRetries);
82
+ await delay(1000);
108
83
  continue;
109
84
  }
110
- else {
111
- logError(`[Supastash] Remote sync failed on ${state.table} with ${state.method} after ${retryCount + 1} tries: ${error.message}`);
85
+ calledOfflineRetries.delete(opKey);
86
+ const { error } = await querySupabase({ ...state }, true);
87
+ if (!error) {
88
+ log(`[Supastash] Synced successfully: ${opKey}`);
89
+ break;
112
90
  }
91
+ const currentRetry = (retryCount.get(opKey) ?? 0) + 1;
92
+ logError(`[Supastash] Remote sync failed: ${opKey} (${currentRetry}/${MAX_RETRIES}) → ${error.message}`);
93
+ retryCount.set(opKey, currentRetry);
94
+ await delay(100 * currentRetry);
113
95
  }
114
96
  }
115
- isRunning = false;
116
- }
117
- function delay(ms) {
118
- return new Promise((res) => setTimeout(res, ms));
119
- }
120
- let batchTimer = null;
121
- function addToCache(state) {
122
- enqueueState(state);
123
- if (batchTimer)
124
- clearTimeout(batchTimer);
125
- batchTimer = setTimeout(() => {
126
- runBatchedRemoteQuery();
127
- }, 200);
97
+ catch (error) {
98
+ logError(`[Supastash] Error running remote query: ${opKey} → ${error}`);
99
+ }
100
+ finally {
101
+ retryCount.delete(opKey);
102
+ stateCache.delete(opKey);
103
+ runningStates.delete(opKey);
104
+ }
128
105
  }
129
106
  export async function runSyncStrategy(state) {
130
107
  const { type } = state;
@@ -1 +1 @@
1
- {"version":3,"file":"queryFilterBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/utils/query/helpers/remoteDb/queryFilterBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAG5D;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;;;EAmC7D"}
1
+ {"version":3,"file":"queryFilterBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/utils/query/helpers/remoteDb/queryFilterBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAG5D;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;;;EAqD7D"}
@@ -16,8 +16,15 @@ export function buildWhereClause(filters) {
16
16
  case "IN":
17
17
  if (!Array.isArray(value) || value.length === 0)
18
18
  continue;
19
+ const isValid = safeValue.every((v) => typeof v === "string" ||
20
+ typeof v === "number" ||
21
+ typeof v === "boolean" ||
22
+ v === null);
23
+ if (!isValid) {
24
+ throw new Error(`❌ IN clause only supports strings, numbers, or booleans. You passed: ${JSON.stringify(value, null, 2)}`);
25
+ }
19
26
  clauseParts.push(`${column} IN (${value.map(() => "?").join(", ")})`);
20
- values.push(...safeValue);
27
+ values.push(...value);
21
28
  break;
22
29
  case "IS":
23
30
  if (value === null) {
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/utils/serializer.ts"],"names":[],"mappings":"AAqBA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAI5C"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/utils/serializer.ts"],"names":[],"mappings":"AAqBA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAuB5C"}
@@ -26,6 +26,20 @@ function stableStringify(obj) {
26
26
  export function getSafeValue(value) {
27
27
  if (value === null || value === undefined)
28
28
  return null;
29
+ if (value instanceof Date)
30
+ return value.toISOString();
31
+ if (Array.isArray(value)) {
32
+ const allPrimitives = value.every((v) => typeof v === "string" ||
33
+ typeof v === "number" ||
34
+ typeof v === "boolean" ||
35
+ v === null);
36
+ if (allPrimitives)
37
+ return value;
38
+ const allObjects = value.every((v) => typeof v === "object" && v !== null);
39
+ if (allObjects)
40
+ return value.map(stableStringify);
41
+ return stableStringify(value);
42
+ }
29
43
  if (typeof value === "object")
30
44
  return stableStringify(value);
31
45
  return value;
@@ -11,7 +11,7 @@ const CHUNK_SIZE = 500;
11
11
  async function permanentlyDeleteChunkLocally(table, chunk) {
12
12
  const db = await getSupastashDb();
13
13
  for (const row of chunk) {
14
- await db.runAsync(`DELETE FROM ${table} WHERE id = ${row.id}`);
14
+ await db.runAsync(`DELETE FROM ${table} WHERE id = ?`, [row.id]);
15
15
  }
16
16
  }
17
17
  function errorHandler(error, table, attempts) {
@@ -47,7 +47,7 @@ async function deleteChunk(table, chunk) {
47
47
  * @param unsyncedRecords - The unsynced records to delete
48
48
  */
49
49
  export async function deleteData(table, unsyncedRecords) {
50
- const cleanRecords = unsyncedRecords.map(({ synced_at, deleted_at, ...rest }) => parseStringifiedFields(rest));
50
+ const cleanRecords = unsyncedRecords.map(({ synced_at, ...rest }) => parseStringifiedFields(rest));
51
51
  for (let i = 0; i < cleanRecords.length; i += CHUNK_SIZE) {
52
52
  const chunk = cleanRecords.slice(i, i + CHUNK_SIZE);
53
53
  await deleteChunk(table, chunk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supastash",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",