supastash 0.1.34 → 0.1.35

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 (72) hide show
  1. package/dist/constants/syncDefaults.d.ts +5 -0
  2. package/dist/constants/syncDefaults.d.ts.map +1 -0
  3. package/dist/constants/syncDefaults.js +19 -0
  4. package/dist/core/config/index.d.ts.map +1 -1
  5. package/dist/core/config/index.js +20 -0
  6. package/dist/hooks/supastashData/fetchCalls.js +2 -2
  7. package/dist/hooks/supastashFilters/index.d.ts.map +1 -1
  8. package/dist/hooks/supastashFilters/index.js +33 -21
  9. package/dist/hooks/syncEngine/index.d.ts +14 -23
  10. package/dist/hooks/syncEngine/index.d.ts.map +1 -1
  11. package/dist/hooks/syncEngine/index.js +177 -65
  12. package/dist/hooks/syncEngine/pullFromRemote/index.d.ts.map +1 -1
  13. package/dist/hooks/syncEngine/pullFromRemote/index.js +12 -3
  14. package/dist/hooks/syncEngine/pushLocal/index.d.ts.map +1 -1
  15. package/dist/hooks/syncEngine/pushLocal/index.js +43 -14
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/store/syncCalls.d.ts +2 -4
  20. package/dist/store/syncCalls.d.ts.map +1 -1
  21. package/dist/types/supastashConfig.types.d.ts +161 -0
  22. package/dist/types/syncEngine.types.d.ts +15 -0
  23. package/dist/utils/getSafeValues.d.ts +2 -0
  24. package/dist/utils/getSafeValues.d.ts.map +1 -0
  25. package/dist/utils/getSafeValues.js +129 -0
  26. package/dist/utils/query/helpers/localDb/getLocalMethod.d.ts +2 -2
  27. package/dist/utils/query/helpers/localDb/getLocalMethod.d.ts.map +1 -1
  28. package/dist/utils/query/helpers/localDb/getLocalMethod.js +2 -2
  29. package/dist/utils/query/helpers/localDb/localQueryBuilder.d.ts +2 -2
  30. package/dist/utils/query/helpers/localDb/localQueryBuilder.d.ts.map +1 -1
  31. package/dist/utils/query/helpers/localDb/localQueryBuilder.js +2 -2
  32. package/dist/utils/query/helpers/localDb/upsertMany.d.ts +2 -2
  33. package/dist/utils/query/helpers/localDb/upsertMany.d.ts.map +1 -1
  34. package/dist/utils/query/helpers/localDb/upsertMany.js +28 -6
  35. package/dist/utils/query/helpers/mainQueryHelpers.d.ts +0 -1
  36. package/dist/utils/query/helpers/mainQueryHelpers.d.ts.map +1 -1
  37. package/dist/utils/query/helpers/mainQueryHelpers.js +27 -96
  38. package/dist/utils/query/helpers/queueRemote.d.ts +3 -0
  39. package/dist/utils/query/helpers/queueRemote.d.ts.map +1 -0
  40. package/dist/utils/query/helpers/queueRemote.js +95 -0
  41. package/dist/utils/query/localDbQuery/index.d.ts.map +1 -1
  42. package/dist/utils/query/localDbQuery/index.js +1 -1
  43. package/dist/utils/query/localDbQuery/upsert.d.ts +2 -2
  44. package/dist/utils/query/localDbQuery/upsert.d.ts.map +1 -1
  45. package/dist/utils/query/localDbQuery/upsert.js +2 -2
  46. package/dist/utils/query/remoteQuery/supabaseQuery.d.ts.map +1 -1
  47. package/dist/utils/query/remoteQuery/supabaseQuery.js +3 -2
  48. package/dist/utils/sync/pullFromRemote/runLimitedConcurrency.d.ts +9 -0
  49. package/dist/utils/sync/pullFromRemote/runLimitedConcurrency.d.ts.map +1 -0
  50. package/dist/utils/sync/pullFromRemote/runLimitedConcurrency.js +33 -0
  51. package/dist/utils/sync/pullFromRemote/updateLocalDb.d.ts.map +1 -1
  52. package/dist/utils/sync/pullFromRemote/updateLocalDb.js +44 -37
  53. package/dist/utils/sync/pushLocal/deleteChunks.d.ts.map +1 -1
  54. package/dist/utils/sync/pushLocal/deleteChunks.js +31 -28
  55. package/dist/utils/sync/pushLocal/normalize.d.ts +3 -0
  56. package/dist/utils/sync/pushLocal/normalize.d.ts.map +1 -0
  57. package/dist/utils/sync/pushLocal/normalize.js +27 -0
  58. package/dist/utils/sync/pushLocal/sendUnsyncedToSupabase.d.ts +3 -3
  59. package/dist/utils/sync/pushLocal/sendUnsyncedToSupabase.d.ts.map +1 -1
  60. package/dist/utils/sync/pushLocal/sendUnsyncedToSupabase.js +17 -10
  61. package/dist/utils/sync/pushLocal/uploadChunk.d.ts.map +1 -1
  62. package/dist/utils/sync/pushLocal/uploadChunk.js +89 -103
  63. package/dist/utils/sync/pushLocal/uploadHelpers.d.ts +17 -0
  64. package/dist/utils/sync/pushLocal/uploadHelpers.d.ts.map +1 -0
  65. package/dist/utils/sync/pushLocal/uploadHelpers.js +197 -0
  66. package/dist/utils/sync/registration/syncCalls.d.ts +25 -0
  67. package/dist/utils/sync/registration/syncCalls.d.ts.map +1 -0
  68. package/dist/utils/sync/registration/syncCalls.js +37 -0
  69. package/dist/utils/syncStatus.d.ts +1 -0
  70. package/dist/utils/syncStatus.d.ts.map +1 -1
  71. package/dist/utils/syncStatus.js +15 -1
  72. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ import { FieldEnforcement, SupastashSyncPolicy } from "../types/supastashConfig.types";
2
+ export declare const DEFAULT_POLICY: SupastashSyncPolicy;
3
+ export declare const DEFAULT_FIELDS: FieldEnforcement;
4
+ export declare const DEFAULT_CHUNK_SIZE = 500;
5
+ //# sourceMappingURL=syncDefaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncDefaults.d.ts","sourceRoot":"","sources":["../../src/constants/syncDefaults.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAExC,eAAO,MAAM,cAAc,EAAE,mBAU5B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,gBAO5B,CAAC;AAEF,eAAO,MAAM,kBAAkB,MAAM,CAAC"}
@@ -0,0 +1,19 @@
1
+ export const DEFAULT_POLICY = {
2
+ nonRetryableCodes: new Set(["23505", "23502", "23514", "23P01"]),
3
+ retryableCodes: new Set(["40001", "40P01", "55P03"]),
4
+ fkCode: "23503",
5
+ onNonRetryable: "accept-server",
6
+ maxTransientMs: 20 * 60 * 1000, // 20m
7
+ maxFkBlockMs: 24 * 60 * 60 * 1000, // 24h
8
+ backoffDelaysMs: [10000, 30000, 120000, 300000, 600000],
9
+ maxBatchAttempts: 5,
10
+ };
11
+ export const DEFAULT_FIELDS = {
12
+ requireCreatedAt: true,
13
+ requireUpdatedAt: true,
14
+ createdAtField: "created_at",
15
+ updatedAtField: "updated_at",
16
+ autoFillMissing: true,
17
+ autoFillDefaultISO: "1970-01-01T00:00:00Z",
18
+ };
19
+ export const DEFAULT_CHUNK_SIZE = 500;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AAuB3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,0BAA0B,EACrE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,gBAAgB,EAAE,CAAC,CAAA;CAAE,QAqBrD;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/core/config/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AA0B3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,0BAA0B,EACrE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,gBAAgB,EAAE,CAAC,CAAA;CAAE,QAuCrD;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,3 +1,4 @@
1
+ import { DEFAULT_FIELDS, DEFAULT_POLICY } from "../../constants/syncDefaults";
1
2
  let _config = {
2
3
  dbName: "supastash_db",
3
4
  supabaseClient: null,
@@ -15,6 +16,9 @@ let _config = {
15
16
  },
16
17
  listeners: 250,
17
18
  debugMode: true,
19
+ syncPolicy: DEFAULT_POLICY,
20
+ fieldEnforcement: DEFAULT_FIELDS,
21
+ deleteConflictedRows: false,
18
22
  };
19
23
  let _configured = false;
20
24
  /**
@@ -76,6 +80,22 @@ export function configureSupastash(config) {
76
80
  pull: config.pollingInterval?.pull ?? _config.pollingInterval?.pull ?? 30000,
77
81
  push: config.pollingInterval?.push ?? _config.pollingInterval?.push ?? 30000,
78
82
  },
83
+ syncPolicy: {
84
+ ...DEFAULT_POLICY,
85
+ ..._config.syncPolicy,
86
+ ...config.syncPolicy,
87
+ nonRetryableCodes: config.syncPolicy?.nonRetryableCodes ??
88
+ _config.syncPolicy?.nonRetryableCodes ??
89
+ DEFAULT_POLICY.nonRetryableCodes,
90
+ retryableCodes: config.syncPolicy?.retryableCodes ??
91
+ _config.syncPolicy?.retryableCodes ??
92
+ DEFAULT_POLICY.retryableCodes,
93
+ },
94
+ fieldEnforcement: {
95
+ ...DEFAULT_FIELDS,
96
+ ..._config.fieldEnforcement,
97
+ ...config.fieldEnforcement,
98
+ },
79
99
  };
80
100
  _configured = true;
81
101
  }
@@ -11,13 +11,13 @@ export function fetchCalls(table, options, initialized) {
11
11
  if (filter && useFilterWhileSyncing && !tableFiltersUsed.has(table)) {
12
12
  tableFilters.set(table, [filter]);
13
13
  }
14
- if (onPushToRemote) {
14
+ if (onPushToRemote && !syncCalls.get(table)?.push) {
15
15
  syncCalls.set(table, {
16
16
  ...(syncCalls.get(table) || {}),
17
17
  push: onPushToRemote,
18
18
  });
19
19
  }
20
- if (onInsertAndUpdate) {
20
+ if (onInsertAndUpdate && !syncCalls.get(table)?.pull) {
21
21
  syncCalls.set(table, {
22
22
  ...(syncCalls.get(table) || {}),
23
23
  pull: onInsertAndUpdate,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/supastashFilters/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAarE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,OAAO,EAAE,eAAe,QA6BnE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/supastashFilters/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAarE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,OAAO,EAAE,eAAe,QAmDnE"}
@@ -1,5 +1,5 @@
1
1
  import { useEffect } from "react";
2
- import { tableFilters, tableFiltersUsed } from "../../store/tableFilters";
2
+ import { filterTracker, tableFilters, tableFiltersUsed, } from "../../store/tableFilters";
3
3
  import { logWarn } from "../../utils/logs";
4
4
  import isValidFilter, { warnOnMisMatch, } from "../../utils/sync/pullFromRemote/validateFilters";
5
5
  import { checkIfTableExist } from "../../utils/tableValidator";
@@ -41,31 +41,43 @@ function warnInvalidFilter(filter, table) {
41
41
  */
42
42
  export default function useSupastashFilters(filters) {
43
43
  useEffect(() => {
44
- const addFilters = async () => {
45
- for (const table in filters) {
46
- const tableInDb = await checkIfTableExist(table);
47
- if (!tableInDb) {
48
- logWarn(`Table ${table} does not exist`);
49
- continue;
44
+ let cancelled = false;
45
+ async function run() {
46
+ const incoming = Object.keys(filters ?? {});
47
+ // Remove stale tables
48
+ for (const t of Array.from(tableFilters.keys())) {
49
+ if (!incoming.includes(t)) {
50
+ tableFilters.delete(t);
51
+ tableFiltersUsed.delete(t);
52
+ filterTracker.delete(t);
50
53
  }
51
- const filtersForTable = filters[table];
52
- if (!filtersForTable) {
53
- logWarn(`No filters for table ${table}`);
54
+ }
55
+ // Existence check + per-table registration
56
+ const existence = await Promise.all(incoming.map(async (t) => [t, await checkIfTableExist(t)]));
57
+ if (cancelled)
58
+ return;
59
+ for (const [table, exists] of existence) {
60
+ if (!exists) {
61
+ logWarn(`Table '${table}' does not exist; skipping filters`);
54
62
  continue;
55
63
  }
56
- const validFilters = filtersForTable.filter((f) => {
57
- const isValid = isValidFilter([f]);
58
- if (!isValid)
59
- warnInvalidFilter(f, table);
60
- return isValid;
61
- });
62
- if (validFilters.length > 0) {
63
- warnOnMisMatch(table, validFilters);
64
- tableFilters.set(table, validFilters);
65
- tableFiltersUsed.add(table);
64
+ const raw = filters[table] ?? [];
65
+ const valid = raw.filter((f) => isValidFilter([f]));
66
+ if (!valid.length) {
67
+ tableFilters.delete(table);
68
+ tableFiltersUsed.delete(table);
69
+ filterTracker.delete(table);
70
+ continue;
66
71
  }
72
+ // Warn on signature change and store a cloned array
73
+ warnOnMisMatch(table, valid);
74
+ tableFilters.set(table, valid.map((v) => ({ ...v })));
75
+ tableFiltersUsed.add(table);
67
76
  }
77
+ }
78
+ void run();
79
+ return () => {
80
+ cancelled = true;
68
81
  };
69
- void addFilters();
70
82
  }, [filters]);
71
83
  }
@@ -1,37 +1,28 @@
1
1
  /**
2
- * Syncs the local data to the remote database
2
+ * Push then (optionally) pull.
3
+ * - Single flight across entire app via module-scoped flags.
4
+ * - Both directions gated on connectivity.
5
+ * - "force" ignores pull cadence timing.
3
6
  */
4
7
  export declare function syncAll(force?: boolean): Promise<void>;
8
+ /**
9
+ * Hook to start/stop the periodic sync engine.
10
+ * - Staggers push & pull timers.
11
+ * - Debounced foreground trigger.
12
+ * - Shares module-level single-flight guards with syncAll().
13
+ */
5
14
  export declare function useSyncEngine(): {
6
15
  startSync: () => void;
7
16
  stopSync: () => void;
8
17
  };
9
18
  /**
10
- * Manually syncs a **single** table with Supabase.
11
- *
12
- * This function:
13
- * - Pulls remote data into local SQLite (if a pull handler is registered)
14
- * - Pushes unsynced local data to Supabase
15
- * - Skips syncing if already in progress for this table
16
- * - Applies filter if `useFiltersFromStore` is enabled
17
- *
18
- * Use this for explicit sync triggers (e.g., pull-to-refresh).
19
- *
20
- * @param {string} table - The name of the table to sync.
21
- * @returns {Promise<void>}
19
+ * Manually sync a single table (pull then push for that table).
20
+ * - Uses table-specific handlers from syncCalls if provided.
21
+ * - Respects configured filters when enabled.
22
22
  */
23
23
  export declare function syncTable(table: string): Promise<void>;
24
24
  /**
25
- * Manually syncs **all registered tables** with Supabase.
26
- *
27
- * This function:
28
- * - Triggers a full sync of all registered tables
29
- * - Forces a sync outside the normal polling schedule
30
- * - Skips any table that is already syncing
31
- *
32
- * Useful for global refreshes (e.g., on app foreground).
33
- *
34
- * @returns {Promise<void>}
25
+ * Force a global sync pass now (push then pull if due).
35
26
  */
36
27
  export declare function syncAllTables(): Promise<void>;
37
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/syncEngine/index.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,iBA2BnD;AAED,wBAAgB,aAAa;;;EA+B5B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5D;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/syncEngine/index.ts"],"names":[],"mappings":"AA4BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCnE;AAmDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA8D5B;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,4 +1,4 @@
1
- import { useRef } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
  import { AppState } from "react-native";
3
3
  import { getSupastashConfig } from "../../core/config";
4
4
  import { syncCalls } from "../../store/syncCalls";
@@ -7,103 +7,215 @@ import { isOnline } from "../../utils/connection";
7
7
  import log from "../../utils/logs";
8
8
  import { updateLocalDb } from "../../utils/sync/pullFromRemote/updateLocalDb";
9
9
  import { pushLocalDataToRemote } from "../../utils/sync/pushLocal/sendUnsyncedToSupabase";
10
- import { pullFromRemote } from "./pullFromRemote";
11
- import { pushLocalData } from "./pushLocal";
10
+ import { pullFromRemote as doPullFromRemote } from "./pullFromRemote";
11
+ import { pushLocalData as doPushLocalData } from "./pushLocal";
12
+ // -----------------------------
13
+ // Module-scoped state & tunables
14
+ // -----------------------------
12
15
  let isSyncing = false;
13
- let lastFullSync = 0;
14
- const syncPollingInterval = getSupastashConfig().pollingInterval?.push || 30000;
16
+ let isPushing = false;
17
+ let isPulling = false;
18
+ let lastPullAt = 0;
19
+ let lastPushAt = 0;
20
+ const MIN_FOREGROUND_GAP = 5000; // ms
21
+ // -----------------------------
22
+ // Core orchestration
23
+ // -----------------------------
15
24
  /**
16
- * Syncs the local data to the remote database
25
+ * Push then (optionally) pull.
26
+ * - Single flight across entire app via module-scoped flags.
27
+ * - Both directions gated on connectivity.
28
+ * - "force" ignores pull cadence timing.
17
29
  */
18
30
  export async function syncAll(force = false) {
19
- if (isSyncing) {
31
+ if (isSyncing)
20
32
  return;
21
- }
22
33
  if (!(await isOnline()))
23
34
  return;
35
+ isSyncing = true;
36
+ const started = Date.now();
24
37
  try {
25
- isSyncing = true;
26
- if (getSupastashConfig().syncEngine?.pull) {
38
+ const cfg = getSupastashConfig();
39
+ // PUSH
40
+ if (cfg.syncEngine?.push) {
41
+ await pushLocalDataSafe();
42
+ }
43
+ // PULL
44
+ if (cfg.syncEngine?.pull) {
45
+ const pullInterval = cfg.pollingInterval?.pull ?? 30000;
27
46
  const now = Date.now();
28
- const shouldPull = force ||
29
- now - lastFullSync >
30
- (getSupastashConfig().pollingInterval?.pull || 30000);
31
- if (shouldPull) {
32
- await pullFromRemote();
33
- lastFullSync = now;
47
+ const due = force || now - lastPullAt >= pullInterval;
48
+ if (due) {
49
+ await pullFromRemoteSafe();
50
+ lastPullAt = now;
34
51
  }
35
52
  }
36
- await pushLocalData();
37
53
  }
38
- catch (error) {
39
- log(`[Supastash] Error syncing: ${error}`);
54
+ catch (e) {
55
+ log("[Supastash] Error in syncAll", {
56
+ msg: String(e),
57
+ code: e?.code ?? e?.name ?? "UNKNOWN",
58
+ });
40
59
  }
41
60
  finally {
42
61
  isSyncing = false;
62
+ log(`[Supastash] syncAll finished in ${Date.now() - started}ms`);
63
+ }
64
+ }
65
+ // -----------------------------
66
+ // Directional helpers
67
+ // -----------------------------
68
+ async function pushLocalDataSafe() {
69
+ if (isPushing)
70
+ return;
71
+ if (!(await isOnline()))
72
+ return;
73
+ const cfg = getSupastashConfig();
74
+ if (!cfg.syncEngine?.push)
75
+ return;
76
+ isPushing = true;
77
+ try {
78
+ await doPushLocalData();
79
+ lastPushAt = Date.now();
80
+ }
81
+ catch (e) {
82
+ log("[Supastash] push error", {
83
+ msg: String(e),
84
+ code: e?.code ?? e?.name ?? "UNKNOWN",
85
+ });
86
+ }
87
+ finally {
88
+ isPushing = false;
43
89
  }
44
90
  }
91
+ async function pullFromRemoteSafe() {
92
+ if (isPulling)
93
+ return;
94
+ if (!(await isOnline()))
95
+ return;
96
+ const cfg = getSupastashConfig();
97
+ if (!cfg.syncEngine?.pull)
98
+ return;
99
+ isPulling = true;
100
+ try {
101
+ await doPullFromRemote();
102
+ lastPullAt = Date.now();
103
+ }
104
+ catch (e) {
105
+ log("[Supastash] pull error", {
106
+ msg: String(e),
107
+ code: e?.code ?? e?.name ?? "UNKNOWN",
108
+ });
109
+ }
110
+ finally {
111
+ isPulling = false;
112
+ }
113
+ }
114
+ // -----------------------------
115
+ // React hook: timer & lifecycle management
116
+ // -----------------------------
117
+ /**
118
+ * Hook to start/stop the periodic sync engine.
119
+ * - Staggers push & pull timers.
120
+ * - Debounced foreground trigger.
121
+ * - Shares module-level single-flight guards with syncAll().
122
+ */
45
123
  export function useSyncEngine() {
46
- const isSyncingRef = useRef(false);
47
- const intervalRef = useRef(null);
48
- const appStateRef = useRef(null);
124
+ // Timers & lifecycle
125
+ const intervalRefPush = useRef(null);
126
+ const intervalRefPull = useRef(null);
127
+ const appStateSubRef = useRef(null);
128
+ // Debounce foreground re-entry
129
+ const lastForeground = useRef(0);
49
130
  function startSync() {
50
- if (isSyncingRef.current)
131
+ if (intervalRefPush.current ||
132
+ intervalRefPull.current ||
133
+ appStateSubRef.current) {
51
134
  return;
52
- isSyncingRef.current = true;
53
- syncAll(true);
54
- const config = getSupastashConfig();
55
- const syncPollingInterval = config.pollingInterval?.push ?? 30000;
56
- intervalRef.current = setInterval(() => {
57
- syncAll();
58
- }, syncPollingInterval);
59
- appStateRef.current = AppState.addEventListener("change", (state) => {
60
- if (state === "active") {
61
- syncAll(true);
62
- }
135
+ }
136
+ // Kick a one-shot global sync on start
137
+ void syncAll(true);
138
+ const cfg = getSupastashConfig();
139
+ const pushEvery = cfg.pollingInterval?.push ?? 30000;
140
+ const pullEvery = cfg.pollingInterval?.pull ?? 30000;
141
+ // Push ticker
142
+ intervalRefPush.current = setInterval(() => {
143
+ void pushLocalDataSafe();
144
+ }, pushEvery);
145
+ // Pull ticker
146
+ intervalRefPull.current = setInterval(() => {
147
+ void pullFromRemoteSafe();
148
+ }, pullEvery + 500);
149
+ // Foreground trigger
150
+ appStateSubRef.current = AppState.addEventListener("change", (state) => {
151
+ if (state !== "active")
152
+ return;
153
+ const now = Date.now();
154
+ if (now - lastForeground.current < MIN_FOREGROUND_GAP)
155
+ return;
156
+ lastForeground.current = now;
157
+ void syncAll(true);
63
158
  });
64
159
  }
65
160
  function stopSync() {
66
- if (intervalRef.current)
67
- clearInterval(intervalRef.current);
68
- appStateRef.current?.remove?.();
69
- isSyncingRef.current = false;
161
+ if (intervalRefPush.current) {
162
+ clearInterval(intervalRefPush.current);
163
+ intervalRefPush.current = null;
164
+ }
165
+ if (intervalRefPull.current) {
166
+ clearInterval(intervalRefPull.current);
167
+ intervalRefPull.current = null;
168
+ }
169
+ appStateSubRef.current?.remove?.();
170
+ appStateSubRef.current = null;
70
171
  }
172
+ // Auto-cleanup if the hook lives in a component lifecycle
173
+ useEffect(() => stopSync, []);
71
174
  return { startSync, stopSync };
72
175
  }
176
+ // -----------------------------
177
+ // Manual triggers
178
+ // -----------------------------
73
179
  /**
74
- * Manually syncs a **single** table with Supabase.
75
- *
76
- * This function:
77
- * - Pulls remote data into local SQLite (if a pull handler is registered)
78
- * - Pushes unsynced local data to Supabase
79
- * - Skips syncing if already in progress for this table
80
- * - Applies filter if `useFiltersFromStore` is enabled
81
- *
82
- * Use this for explicit sync triggers (e.g., pull-to-refresh).
83
- *
84
- * @param {string} table - The name of the table to sync.
85
- * @returns {Promise<void>}
180
+ * Manually sync a single table (pull then push for that table).
181
+ * - Uses table-specific handlers from syncCalls if provided.
182
+ * - Respects configured filters when enabled.
86
183
  */
87
184
  export async function syncTable(table) {
88
- const config = getSupastashConfig();
89
- const { useFiltersFromStore = true } = config?.syncEngine || {};
185
+ if (!(await isOnline()))
186
+ return;
187
+ const cfg = getSupastashConfig();
188
+ const { useFiltersFromStore = true } = cfg?.syncEngine || {};
90
189
  const filter = useFiltersFromStore ? tableFilters.get(table) : undefined;
91
- if (getSupastashConfig().syncEngine?.pull) {
92
- await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
190
+ // Pull
191
+ if (cfg.syncEngine?.pull) {
192
+ try {
193
+ await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
194
+ }
195
+ catch (e) {
196
+ log("[Supastash] syncTable pull error", {
197
+ table,
198
+ msg: String(e),
199
+ code: e?.code ?? e?.name ?? "UNKNOWN",
200
+ });
201
+ }
202
+ }
203
+ // Push (use table handler if present)
204
+ if (cfg.syncEngine?.push) {
205
+ try {
206
+ await pushLocalDataToRemote(table, syncCalls.get(table)?.push);
207
+ }
208
+ catch (e) {
209
+ log("[Supastash] syncTable push error", {
210
+ table,
211
+ msg: String(e),
212
+ code: e?.code ?? e?.name ?? "UNKNOWN",
213
+ });
214
+ }
93
215
  }
94
- await pushLocalDataToRemote(table, syncCalls.get(table)?.push);
95
216
  }
96
217
  /**
97
- * Manually syncs **all registered tables** with Supabase.
98
- *
99
- * This function:
100
- * - Triggers a full sync of all registered tables
101
- * - Forces a sync outside the normal polling schedule
102
- * - Skips any table that is already syncing
103
- *
104
- * Useful for global refreshes (e.g., on app foreground).
105
- *
106
- * @returns {Promise<void>}
218
+ * Force a global sync pass now (push then pull if due).
107
219
  */
108
220
  export async function syncAllTables() {
109
221
  await syncAll(true);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/syncEngine/pullFromRemote/index.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,wBAAsB,cAAc,kBAyBnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/syncEngine/pullFromRemote/index.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,wBAAsB,cAAc,kBA8BnC"}
@@ -3,6 +3,7 @@ import { syncCalls } from "../../../store/syncCalls";
3
3
  import { tableFilters } from "../../../store/tableFilters";
4
4
  import log from "../../../utils/logs";
5
5
  import { getAllTables } from "../../../utils/sync/getAllTables";
6
+ import { runLimitedConcurrency } from "../../../utils/sync/pullFromRemote/runLimitedConcurrency";
6
7
  import { updateLocalDb } from "../../../utils/sync/pullFromRemote/updateLocalDb";
7
8
  /**
8
9
  * Pulls the data from the remote database to the local database
@@ -16,9 +17,17 @@ export async function pullFromRemote() {
16
17
  }
17
18
  const excludeTables = getSupastashConfig()?.excludeTables?.pull || [];
18
19
  const tablesToPull = tables.filter((table) => !excludeTables?.includes(table));
19
- for (const table of tablesToPull) {
20
- await updateLocalDb(table, tableFilters.get(table), syncCalls.get(table)?.pull);
21
- }
20
+ const toPull = tablesToPull.map((table) => async () => {
21
+ try {
22
+ const filter = tableFilters.get(table);
23
+ const onReceiveRecord = syncCalls.get(table)?.pull;
24
+ await updateLocalDb(table, filter, onReceiveRecord);
25
+ }
26
+ catch (e) {
27
+ log(`[Supastash] pull table failed: ${table} — ${e?.code ?? e?.name ?? e}`);
28
+ }
29
+ });
30
+ await runLimitedConcurrency(toPull, 3);
22
31
  }
23
32
  catch (error) {
24
33
  log(`[Supastash] Error pulling from remote: ${error}`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/syncEngine/pushLocal/index.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,wBAAsB,aAAa,kBAmClC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/syncEngine/pushLocal/index.ts"],"names":[],"mappings":"AAaA;;GAEG;AACH,wBAAsB,aAAa,kBA4DlC"}
@@ -1,10 +1,13 @@
1
1
  import { getSupastashConfig } from "../../../core/config";
2
2
  import { syncCalls } from "../../../store/syncCalls";
3
+ import { isOnline } from "../../../utils/connection";
3
4
  import log from "../../../utils/logs";
4
5
  import { getAllTables } from "../../../utils/sync/getAllTables";
6
+ import { runLimitedConcurrency } from "../../../utils/sync/pullFromRemote/runLimitedConcurrency";
5
7
  import { pushLocalDataToRemote } from "../../../utils/sync/pushLocal/sendUnsyncedToSupabase";
6
- let timesPushed = 0;
7
- let lastPushed = 0;
8
+ let emptyPassCount = 0;
9
+ let lastEmptyPassAt = 0;
10
+ const tablePushLock = new Map();
8
11
  /**
9
12
  * Pushes the local data to the remote database
10
13
  */
@@ -12,23 +15,49 @@ export async function pushLocalData() {
12
15
  try {
13
16
  const tables = await getAllTables();
14
17
  if (!tables) {
15
- log("No tables found");
18
+ log("[Supastash] No tables found");
16
19
  return;
17
20
  }
21
+ if (!(await isOnline()))
22
+ return;
18
23
  const excludeTables = getSupastashConfig()?.excludeTables?.push || [];
19
24
  const tablesToPush = tables.filter((table) => !excludeTables?.includes(table));
20
- const noSync = [];
21
- for (const table of tablesToPush) {
22
- await pushLocalDataToRemote(table, syncCalls.get(table)?.push, noSync);
23
- }
24
- if (noSync.length > 0) {
25
- timesPushed++;
26
- if (timesPushed >= 150) {
27
- const timeSinceLastPush = Date.now() - lastPushed;
28
- lastPushed = Date.now();
29
- log(`[Supastash] No sync data found for tables: ${noSync.join(", ")} (times pushed: ${timesPushed}) in the last ${timeSinceLastPush}ms`);
30
- timesPushed = 0;
25
+ const results = [];
26
+ const jobs = tablesToPush.map((table) => async () => {
27
+ if (tablePushLock.get(table)) {
28
+ results.push({ table, hadWork: false });
29
+ return;
30
+ }
31
+ tablePushLock.set(table, true);
32
+ try {
33
+ const onPush = syncCalls.get(table)?.push;
34
+ const hadWork = await pushLocalDataToRemote(table, onPush);
35
+ results.push({ table, hadWork: !!hadWork });
36
+ }
37
+ catch (e) {
38
+ const msg = e?.code ?? e?.name ?? String(e);
39
+ results.push({ table, hadWork: false, error: msg });
40
+ log(`[Supastash] Push table failed: ${table} — ${msg}`);
31
41
  }
42
+ finally {
43
+ tablePushLock.set(table, false);
44
+ }
45
+ });
46
+ await runLimitedConcurrency(jobs, 3);
47
+ const hadAnyWork = results.some((r) => r.hadWork);
48
+ if (!hadAnyWork) {
49
+ emptyPassCount += 1;
50
+ if (emptyPassCount % 150 === 0) {
51
+ const now = Date.now();
52
+ const gap = lastEmptyPassAt ? now - lastEmptyPassAt : 0;
53
+ lastEmptyPassAt = now;
54
+ const noSyncTables = results.map((r) => r.table).join(", ");
55
+ log(`[Supastash] No pushable data for: ${noSyncTables} (empty passes: ${emptyPassCount})${gap ? ` in the last ${gap}ms` : ""}`);
56
+ }
57
+ }
58
+ else {
59
+ emptyPassCount = 0;
60
+ lastEmptyPassAt = Date.now();
32
61
  }
33
62
  }
34
63
  catch (error) {
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { supastash } from "./utils/query/builder";
10
10
  export { dropAllTables, dropTable, wipeAllTables, wipeOldDataForAllTables, wipeOldDataForATable, wipeTable, } from "./utils/schema/wipeTables";
11
11
  export { getAllTables } from "./utils/sync/getAllTables";
12
12
  export { refreshAllTables, refreshTable, refreshTableWithPayload, } from "./utils/sync/refreshTables";
13
+ export { clearSyncCalls, getAllSyncTables, getSyncCall, registerSyncCall, unregisterSyncCall, } from "./utils/sync/registration/syncCalls";
13
14
  export { clearAllLocalDeleteLog, clearAllLocalSyncLog, clearLocalDeleteLog, clearLocalSyncLog, getLocalDeleteLog, getLocalSyncLog, setLocalDeleteLog, setLocalSyncLog, } from "./utils/syncStatus";
14
15
  export type { CrudMethods } from "./types/query.types";
15
16
  export type { RealtimeOptions, SupastashDataResult, SupastashFilter, } from "./types/realtimeData.types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,oBAAoB,EACpB,SAAS,GACV,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,oBAAoB,EACpB,SAAS,GACV,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,+BAA+B,CAAC"}