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.
- package/dist/utils/query/helpers/mainQueryHelpers.d.ts +1 -0
- package/dist/utils/query/helpers/mainQueryHelpers.d.ts.map +1 -1
- package/dist/utils/query/helpers/mainQueryHelpers.js +47 -70
- package/dist/utils/query/helpers/remoteDb/queryFilterBuilder.d.ts.map +1 -1
- package/dist/utils/query/helpers/remoteDb/queryFilterBuilder.js +8 -1
- package/dist/utils/serializer.d.ts.map +1 -1
- package/dist/utils/serializer.js +14 -0
- package/dist/utils/sync/pushLocal/deleteChunks.js +2 -2
- package/package.json +1 -1
|
@@ -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;
|
|
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,
|
|
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
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
63
|
+
runningStates.add(opKey);
|
|
64
|
+
stateCache.set(opKey, state);
|
|
65
|
+
runRemoteQuery(opKey);
|
|
61
66
|
}
|
|
62
|
-
async function
|
|
63
|
-
|
|
67
|
+
async function runRemoteQuery(opKey) {
|
|
68
|
+
const state = stateCache.get(opKey);
|
|
69
|
+
if (!state)
|
|
64
70
|
return;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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;;;
|
|
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(...
|
|
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,
|
|
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"}
|
package/dist/utils/serializer.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
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);
|