local-lite 0.0.3 → 0.0.5

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 (63) hide show
  1. package/README.md +421 -362
  2. package/dist/adapters/local/better-sqlite3.d.ts +2 -0
  3. package/dist/adapters/local/better-sqlite3.js +46 -25
  4. package/dist/adapters/local/expo-sqlite.d.ts +5 -1
  5. package/dist/adapters/local/expo-sqlite.js +40 -7
  6. package/dist/adapters/local/index.d.ts +1 -0
  7. package/dist/adapters/local/node-sqlite.d.ts +2 -0
  8. package/dist/adapters/local/node-sqlite.js +45 -22
  9. package/dist/adapters/local/op-sqlite.d.ts +1 -0
  10. package/dist/adapters/local/op-sqlite.js +6 -0
  11. package/dist/adapters/local/sql-js.d.ts +1 -0
  12. package/dist/adapters/local/sql-js.js +5 -0
  13. package/dist/adapters/network/index.js +33 -0
  14. package/dist/adapters/remote/convex.d.ts +2 -2
  15. package/dist/adapters/remote/convex.js +8 -12
  16. package/dist/adapters/remote/demo.d.ts +1 -1
  17. package/dist/adapters/remote/demo.js +3 -0
  18. package/dist/adapters/remote/firestore.d.ts +3 -2
  19. package/dist/adapters/remote/firestore.js +69 -46
  20. package/dist/adapters/remote/index.d.ts +6 -4
  21. package/dist/adapters/remote/supabase.d.ts +34 -6
  22. package/dist/adapters/remote/supabase.js +340 -79
  23. package/dist/config/index.js +7 -10
  24. package/dist/config/resolve-local.js +12 -1
  25. package/dist/config/resolve-remote.js +33 -28
  26. package/dist/config/types.d.ts +30 -5
  27. package/dist/core/client.d.ts +133 -5
  28. package/dist/core/client.js +431 -46
  29. package/dist/core/index.d.ts +15 -0
  30. package/dist/core/index.js +30 -0
  31. package/dist/core/outbox-store.d.ts +8 -4
  32. package/dist/core/outbox-store.js +84 -65
  33. package/dist/core/query-builder.d.ts +119 -0
  34. package/dist/core/query-builder.js +372 -0
  35. package/dist/core/query.d.ts +8 -0
  36. package/dist/core/query.js +37 -0
  37. package/dist/core/schema-manager.d.ts +9 -0
  38. package/dist/core/schema-manager.js +103 -0
  39. package/dist/core/sync-worker.d.ts +38 -1
  40. package/dist/core/sync-worker.js +351 -104
  41. package/dist/core/table-manager.js +26 -29
  42. package/dist/core/types.d.ts +8 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/dist/react/BenchmarkModal.d.ts +32 -0
  46. package/dist/react/BenchmarkModal.js +777 -0
  47. package/dist/react/SyncInspector.d.ts +31 -0
  48. package/dist/react/SyncInspector.js +585 -0
  49. package/dist/react/index.d.ts +89 -18
  50. package/dist/react/index.js +450 -105
  51. package/dist/react/runner.d.ts +7 -0
  52. package/dist/react/runner.js +42 -0
  53. package/dist/react/status.d.ts +24 -0
  54. package/dist/react/status.js +123 -0
  55. package/dist/react/theme.d.ts +18 -0
  56. package/dist/react/theme.js +46 -0
  57. package/dist/schema/generator.d.ts +49 -0
  58. package/dist/schema/generator.js +209 -0
  59. package/dist/schema/index.d.ts +1 -0
  60. package/dist/schema/index.js +17 -0
  61. package/dist/utils/logger.d.ts +4 -0
  62. package/dist/utils/logger.js +20 -0
  63. package/package.json +143 -133
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryBuilder = void 0;
4
+ exports.formatColumnIdentifier = formatColumnIdentifier;
5
+ /**
6
+ * Escapes an identifier or compound identifier (e.g. 'table.column' or 'table.col as alias').
7
+ */
8
+ function formatColumnIdentifier(col) {
9
+ const trimmed = col.trim();
10
+ if (trimmed === '*')
11
+ return '*';
12
+ // Handle "expression AS alias"
13
+ const asMatch = trimmed.match(/^(.+?)\s+(?:AS|as)\s+([A-Za-z0-9_"]+)$/i);
14
+ if (asMatch) {
15
+ const [, expr, rawAlias] = asMatch;
16
+ const unquotedAlias = rawAlias.startsWith('"') && rawAlias.endsWith('"')
17
+ ? rawAlias.slice(1, -1)
18
+ : rawAlias;
19
+ const cleanAlias = `"${unquotedAlias.replace(/"/g, '""')}"`;
20
+ if (expr.includes('(') && expr.endsWith(')')) {
21
+ return `${expr} AS ${cleanAlias}`;
22
+ }
23
+ return `${formatColumnIdentifier(expr)} AS ${cleanAlias}`;
24
+ }
25
+ // Handle function calls without alias (e.g. COUNT(*))
26
+ if (trimmed.includes('(') && trimmed.endsWith(')')) {
27
+ return trimmed;
28
+ }
29
+ // Handle "table.column"
30
+ if (trimmed.includes('.')) {
31
+ return trimmed
32
+ .split('.')
33
+ .map((part) => `"${part.replace(/"/g, '""')}"`)
34
+ .join('.');
35
+ }
36
+ return `"${trimmed.replace(/"/g, '""')}"`;
37
+ }
38
+ /**
39
+ * Fluent, type-safe SQL Query Builder for local-first SQLite operations.
40
+ * Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
41
+ */
42
+ class QueryBuilder {
43
+ client;
44
+ tableName;
45
+ selectCols = ['*'];
46
+ joins = [];
47
+ whereConditions = {};
48
+ orderColumn;
49
+ orderDirection = 'ASC';
50
+ limitCount;
51
+ offsetCount;
52
+ groupByCols = [];
53
+ includeDeleted = false;
54
+ constructor(client, tableName) {
55
+ this.client = client;
56
+ this.tableName = tableName;
57
+ }
58
+ /**
59
+ * Specifies which columns to select.
60
+ * Supports column aliases (e.g. ['id', 'contracts.rent_amount AS rent', 'houses.house_number']).
61
+ */
62
+ select(...columns) {
63
+ const flat = columns.flat();
64
+ if (flat.length > 0) {
65
+ this.selectCols = flat;
66
+ }
67
+ return this;
68
+ }
69
+ /**
70
+ * Inner joins another SQLite table on a matching column expression.
71
+ */
72
+ join(table, firstCol, secondCol) {
73
+ this.joins.push({ type: 'INNER', table, firstCol, secondCol });
74
+ return this;
75
+ }
76
+ /**
77
+ * Left joins another SQLite table on a matching column expression.
78
+ */
79
+ leftJoin(table, firstCol, secondCol) {
80
+ this.joins.push({ type: 'LEFT', table, firstCol, secondCol });
81
+ return this;
82
+ }
83
+ /**
84
+ * Adds WHERE criteria to filter results.
85
+ * Supports:
86
+ * - where({ status: 'ACTIVE' })
87
+ * - where('status', 'ACTIVE')
88
+ * - where('end_date', '>=', today)
89
+ */
90
+ where(columnOrFilter, operatorOrValue, value) {
91
+ if (typeof columnOrFilter === 'string') {
92
+ if (value !== undefined) {
93
+ const rawOp = String(operatorOrValue).toLowerCase().trim();
94
+ const opKey = rawOp === '>=' ? '$gte' :
95
+ rawOp === '<=' ? '$lte' :
96
+ rawOp === '>' ? '$gt' :
97
+ rawOp === '<' ? '$lt' :
98
+ rawOp === '!=' || rawOp === '<>' ? '$ne' :
99
+ rawOp === '=' || rawOp === '==' ? '$eq' :
100
+ rawOp === 'like' ? '$contains' : '$eq';
101
+ const existing = typeof this.whereConditions[columnOrFilter] === 'object' &&
102
+ this.whereConditions[columnOrFilter] !== null
103
+ ? this.whereConditions[columnOrFilter]
104
+ : {};
105
+ this.whereConditions[columnOrFilter] = { ...existing, [opKey]: value };
106
+ }
107
+ else {
108
+ this.whereConditions[columnOrFilter] = operatorOrValue;
109
+ }
110
+ }
111
+ else if (columnOrFilter && typeof columnOrFilter === 'object') {
112
+ this.whereConditions = { ...this.whereConditions, ...columnOrFilter };
113
+ }
114
+ return this;
115
+ }
116
+ /**
117
+ * Orders query results by column and direction.
118
+ */
119
+ orderBy(column, direction = 'ASC') {
120
+ this.orderColumn = column;
121
+ this.orderDirection = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
122
+ return this;
123
+ }
124
+ /**
125
+ * Groups rows by one or more columns for aggregation.
126
+ */
127
+ groupBy(...columns) {
128
+ this.groupByCols = columns.flat();
129
+ return this;
130
+ }
131
+ /**
132
+ * Limits the number of returned records.
133
+ */
134
+ limit(count) {
135
+ this.limitCount = count;
136
+ return this;
137
+ }
138
+ /**
139
+ * Offsets the starting record position.
140
+ */
141
+ offset(count) {
142
+ this.offsetCount = count;
143
+ return this;
144
+ }
145
+ /**
146
+ * Includes soft-deleted rows in results.
147
+ */
148
+ withDeleted(include = true) {
149
+ this.includeDeleted = include;
150
+ return this;
151
+ }
152
+ /**
153
+ * Applies standard QueryOptions (select, where, orderBy, limit, offset, includeDeleted) to this query.
154
+ */
155
+ applyOptions(options) {
156
+ if (!options)
157
+ return this;
158
+ if (options.select)
159
+ this.select(options.select);
160
+ if (options.where && typeof options.where === 'object')
161
+ this.where(options.where);
162
+ if (options.orderBy)
163
+ this.orderBy(options.orderBy, options.order || 'ASC');
164
+ if (typeof options.limit === 'number')
165
+ this.limit(options.limit);
166
+ if (typeof options.offset === 'number')
167
+ this.offset(options.offset);
168
+ if (options.includeDeleted)
169
+ this.withDeleted(true);
170
+ return this;
171
+ }
172
+ /**
173
+ * Compiles the current builder state into an executable SQL string and parameter list.
174
+ */
175
+ toSQL() {
176
+ const params = [];
177
+ const projections = this.selectCols.map(formatColumnIdentifier).join(', ');
178
+ const fromTable = `"${this.tableName.replace(/"/g, '""')}"`;
179
+ let sql = `SELECT ${projections} FROM ${fromTable}`;
180
+ // Append JOINs
181
+ for (const j of this.joins) {
182
+ const joinTable = `"${j.table.replace(/"/g, '""')}"`;
183
+ const col1 = formatColumnIdentifier(j.firstCol);
184
+ const col2 = formatColumnIdentifier(j.secondCol);
185
+ sql += ` ${j.type} JOIN ${joinTable} ON ${col1} = ${col2}`;
186
+ }
187
+ // Build WHERE clauses
188
+ const whereClauses = [];
189
+ if (this.whereConditions && typeof this.whereConditions === 'object') {
190
+ for (const [col, val] of Object.entries(this.whereConditions)) {
191
+ if (col === '$or' || col === '$and')
192
+ continue;
193
+ const safeCol = formatColumnIdentifier(col);
194
+ if (val === null) {
195
+ whereClauses.push(`${safeCol} IS NULL`);
196
+ }
197
+ else if (typeof val === 'boolean') {
198
+ whereClauses.push(`${safeCol} = ?`);
199
+ params.push(val ? 1 : 0);
200
+ }
201
+ else if (typeof val === 'string' || typeof val === 'number') {
202
+ whereClauses.push(`${safeCol} = ?`);
203
+ params.push(val);
204
+ }
205
+ else if (typeof val === 'object' && !Array.isArray(val)) {
206
+ const ops = val;
207
+ const eqVal = ops.$eq !== undefined ? ops.$eq : ops.eq;
208
+ if (eqVal !== undefined) {
209
+ whereClauses.push(`${safeCol} = ?`);
210
+ params.push(typeof eqVal === 'boolean' ? (eqVal ? 1 : 0) : eqVal);
211
+ }
212
+ const neVal = ops.$ne !== undefined ? ops.$ne : ops.ne;
213
+ if (neVal !== undefined) {
214
+ whereClauses.push(`${safeCol} != ?`);
215
+ params.push(typeof neVal === 'boolean' ? (neVal ? 1 : 0) : neVal);
216
+ }
217
+ const gtVal = ops.$gt !== undefined ? ops.$gt : ops.gt;
218
+ if (gtVal !== undefined) {
219
+ whereClauses.push(`${safeCol} > ?`);
220
+ params.push(gtVal);
221
+ }
222
+ const gteVal = ops.$gte !== undefined ? ops.$gte : ops.gte;
223
+ if (gteVal !== undefined) {
224
+ whereClauses.push(`${safeCol} >= ?`);
225
+ params.push(gteVal);
226
+ }
227
+ const ltVal = ops.$lt !== undefined ? ops.$lt : ops.lt;
228
+ if (ltVal !== undefined) {
229
+ whereClauses.push(`${safeCol} < ?`);
230
+ params.push(ltVal);
231
+ }
232
+ const lteVal = ops.$lte !== undefined ? ops.$lte : ops.lte;
233
+ if (lteVal !== undefined) {
234
+ whereClauses.push(`${safeCol} <= ?`);
235
+ params.push(lteVal);
236
+ }
237
+ const inVal = ops.$in !== undefined ? ops.$in : ops.in;
238
+ if (Array.isArray(inVal) && inVal.length > 0) {
239
+ const placeholders = inVal.map(() => '?').join(', ');
240
+ whereClauses.push(`${safeCol} IN (${placeholders})`);
241
+ params.push(...inVal);
242
+ }
243
+ const ninVal = ops.$nin !== undefined ? ops.$nin : ops.nin;
244
+ if (Array.isArray(ninVal) && ninVal.length > 0) {
245
+ const placeholders = ninVal.map(() => '?').join(', ');
246
+ whereClauses.push(`${safeCol} NOT IN (${placeholders})`);
247
+ params.push(...ninVal);
248
+ }
249
+ const containsVal = ops.$contains !== undefined ? ops.$contains : ops.contains;
250
+ if (containsVal !== undefined) {
251
+ whereClauses.push(`${safeCol} LIKE ?`);
252
+ params.push(`%${containsVal}%`);
253
+ }
254
+ const nullVal = ops.$null !== undefined ? ops.$null : ops.null;
255
+ if (nullVal !== undefined) {
256
+ whereClauses.push(nullVal ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ // Push down soft-deletion check on primary table if deleted column exists and not explicitly overridden
262
+ const schemaManager = 'schemaManager' in this.client
263
+ ? this.client.schemaManager
264
+ : this.client.parent?.schemaManager;
265
+ if (!this.includeDeleted && schemaManager?.hasColumn(this.tableName, 'deleted')) {
266
+ const delCol = this.tableName.includes('.')
267
+ ? `${formatColumnIdentifier(this.tableName)}."deleted"`
268
+ : `"${this.tableName.replace(/"/g, '""')}"."deleted"`;
269
+ whereClauses.push(`(${delCol} IS NULL OR (${delCol} != 1 AND ${delCol} != 'true'))`);
270
+ }
271
+ if (whereClauses.length > 0) {
272
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
273
+ }
274
+ // GROUP BY
275
+ if (this.groupByCols.length > 0) {
276
+ sql += ` GROUP BY ${this.groupByCols.map(formatColumnIdentifier).join(', ')}`;
277
+ }
278
+ // ORDER BY
279
+ if (this.orderColumn) {
280
+ sql += ` ORDER BY ${formatColumnIdentifier(this.orderColumn)} ${this.orderDirection}`;
281
+ }
282
+ // LIMIT & OFFSET
283
+ if (typeof this.limitCount === 'number' && this.limitCount > 0) {
284
+ sql += ` LIMIT ?`;
285
+ params.push(this.limitCount);
286
+ if (typeof this.offsetCount === 'number' && this.offsetCount > 0) {
287
+ sql += ` OFFSET ?`;
288
+ params.push(this.offsetCount);
289
+ }
290
+ }
291
+ return { sql, params };
292
+ }
293
+ /**
294
+ * Executes the compiled SQL query against SQLite and returns rows.
295
+ */
296
+ async execute() {
297
+ const schemaManager = 'schemaManager' in this.client
298
+ ? this.client.schemaManager
299
+ : this.client.parent?.schemaManager;
300
+ const db = 'db' in this.client ? this.client.db : this.client.tx;
301
+ if (schemaManager && db) {
302
+ await schemaManager.ensureTableIntrospected(db, this.tableName);
303
+ for (const j of this.joins) {
304
+ await schemaManager.ensureTableIntrospected(db, j.table);
305
+ }
306
+ }
307
+ const { sql, params } = this.toSQL();
308
+ try {
309
+ const rawRows = await this.client.query(sql, params);
310
+ const hydrator = 'hydrateRow' in this.client
311
+ ? this.client.hydrateRow.bind(this.client)
312
+ : this.client.parent?.hydrateRow?.bind(this.client.parent);
313
+ if (hydrator && rawRows && rawRows.length > 0) {
314
+ return rawRows.map((r) => hydrator(this.tableName, r));
315
+ }
316
+ return rawRows;
317
+ }
318
+ catch (err) {
319
+ const msg = String(err?.message || err || '');
320
+ if (msg.includes('no such table') || msg.includes('no such column')) {
321
+ return [];
322
+ }
323
+ throw err;
324
+ }
325
+ }
326
+ /**
327
+ * Returns the first matching record or null if not found.
328
+ */
329
+ async first() {
330
+ const originalLimit = this.limitCount;
331
+ this.limitCount = 1;
332
+ const rows = await this.execute();
333
+ this.limitCount = originalLimit;
334
+ return rows.length > 0 ? rows[0] : null;
335
+ }
336
+ /**
337
+ * Executes a COUNT(*) in SQLite and returns the numeric count directly.
338
+ */
339
+ async count() {
340
+ const originalSelect = this.selectCols;
341
+ this.selectCols = ['COUNT(*) AS _count'];
342
+ const rows = await this.execute();
343
+ this.selectCols = originalSelect;
344
+ const countVal = rows[0]?._count;
345
+ return typeof countVal === 'number' ? countVal : Number(countVal) || 0;
346
+ }
347
+ /**
348
+ * Executes a SUM(column) in SQLite and returns the numeric total directly.
349
+ */
350
+ async sum(column) {
351
+ const originalSelect = this.selectCols;
352
+ const safeCol = formatColumnIdentifier(column);
353
+ this.selectCols = [`COALESCE(SUM(${safeCol}), 0) AS _sum`];
354
+ const rows = await this.execute();
355
+ this.selectCols = originalSelect;
356
+ const sumVal = rows[0]?._sum;
357
+ return typeof sumVal === 'number' ? sumVal : Number(sumVal) || 0;
358
+ }
359
+ /**
360
+ * Executes an AVG(column) in SQLite and returns the average directly.
361
+ */
362
+ async avg(column) {
363
+ const originalSelect = this.selectCols;
364
+ const safeCol = formatColumnIdentifier(column);
365
+ this.selectCols = [`AVG(${safeCol}) AS _avg`];
366
+ const rows = await this.execute();
367
+ this.selectCols = originalSelect;
368
+ const avgVal = rows[0]?._avg;
369
+ return avgVal === null || avgVal === undefined ? null : Number(avgVal) || 0;
370
+ }
371
+ }
372
+ exports.QueryBuilder = QueryBuilder;
@@ -7,6 +7,14 @@ export declare function matchesField(fieldValue: any, filter: FieldFilter): bool
7
7
  * Evaluates whether a given row matches a WhereFilter.
8
8
  */
9
9
  export declare function matchesWhere(row: Record<string, any>, where?: WhereFilter): boolean;
10
+ /**
11
+ * Checks whether a row is considered soft-deleted.
12
+ */
13
+ export declare function isDeletedRecord(row: any): boolean;
14
+ /**
15
+ * Checks if a where filter explicitly specifies a condition on the 'deleted' column.
16
+ */
17
+ export declare function hasExplicitDeletedFilter(where?: WhereFilter): boolean;
10
18
  /**
11
19
  * Applies in-memory filtering, sorting, offset, and limit to an array of rows.
12
20
  */
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.matchesField = matchesField;
4
4
  exports.matchesWhere = matchesWhere;
5
+ exports.isDeletedRecord = isDeletedRecord;
6
+ exports.hasExplicitDeletedFilter = hasExplicitDeletedFilter;
5
7
  exports.applyQueryOptions = applyQueryOptions;
6
8
  /**
7
9
  * Checks whether a single field value matches a field filter condition.
@@ -106,6 +108,23 @@ function matchesWhere(row, where) {
106
108
  }
107
109
  return true;
108
110
  }
111
+ /**
112
+ * Checks whether a row is considered soft-deleted.
113
+ */
114
+ function isDeletedRecord(row) {
115
+ if (!row || typeof row !== 'object')
116
+ return false;
117
+ const d = row.deleted;
118
+ return d === 1 || d === true || d === '1' || d === 'true';
119
+ }
120
+ /**
121
+ * Checks if a where filter explicitly specifies a condition on the 'deleted' column.
122
+ */
123
+ function hasExplicitDeletedFilter(where) {
124
+ if (!where || typeof where !== 'object' || Array.isArray(where))
125
+ return false;
126
+ return 'deleted' in where;
127
+ }
109
128
  /**
110
129
  * Applies in-memory filtering, sorting, offset, and limit to an array of rows.
111
130
  */
@@ -113,6 +132,11 @@ function applyQueryOptions(rows, options) {
113
132
  if (!rows || rows.length === 0)
114
133
  return [];
115
134
  let result = [...rows];
135
+ // 0. Automatic soft-delete filtering: omit deleted records unless includeDeleted is true
136
+ // or the where filter explicitly targets 'deleted'.
137
+ if (!options?.includeDeleted && !hasExplicitDeletedFilter(options?.where)) {
138
+ result = result.filter((row) => !isDeletedRecord(row));
139
+ }
116
140
  // 1. Where filtering
117
141
  if (options?.where) {
118
142
  result = result.filter((row) => matchesWhere(row, options.where));
@@ -146,5 +170,18 @@ function applyQueryOptions(rows, options) {
146
170
  if (options?.limit !== undefined && options.limit > 0) {
147
171
  result = result.slice(0, options.limit);
148
172
  }
173
+ // 4. Column Projection
174
+ if (options?.select && Array.isArray(options.select) && options.select.length > 0) {
175
+ const fields = options.select;
176
+ result = result.map((row) => {
177
+ const projected = {};
178
+ for (const field of fields) {
179
+ if (field in row) {
180
+ projected[field] = row[field];
181
+ }
182
+ }
183
+ return projected;
184
+ });
185
+ }
149
186
  return result;
150
187
  }
@@ -23,10 +23,19 @@ export declare function formatValueForStorage(value: any): any;
23
23
  export declare class SchemaManager {
24
24
  private knownTables;
25
25
  private knownColumns;
26
+ private knownIndexes;
26
27
  /**
27
28
  * Resets the schema cache (useful for testing or full database resets).
28
29
  */
29
30
  clearCache(): void;
31
+ /**
32
+ * Checks whether a column is known to exist on the specified table.
33
+ */
34
+ hasColumn(tableName: string, column: string): boolean;
35
+ /**
36
+ * Introspects table columns via PRAGMA table_info if not previously cached.
37
+ */
38
+ ensureTableIntrospected(db: DBExecutable, tableName: string): Promise<void>;
30
39
  /**
31
40
  * Ensures that the target table exists in SQLite with the specified primary key.
32
41
  */
@@ -58,12 +58,75 @@ function formatValueForStorage(value) {
58
58
  class SchemaManager {
59
59
  knownTables = new Set();
60
60
  knownColumns = new Set();
61
+ knownIndexes = new Set();
61
62
  /**
62
63
  * Resets the schema cache (useful for testing or full database resets).
63
64
  */
64
65
  clearCache() {
65
66
  this.knownTables.clear();
66
67
  this.knownColumns.clear();
68
+ this.knownIndexes.clear();
69
+ }
70
+ /**
71
+ * Checks whether a column is known to exist on the specified table.
72
+ */
73
+ hasColumn(tableName, column) {
74
+ return this.knownColumns.has(`${tableName}:${column.toLowerCase()}`);
75
+ }
76
+ /**
77
+ * Introspects table columns via PRAGMA table_info if not previously cached.
78
+ */
79
+ async ensureTableIntrospected(db, tableName) {
80
+ const cacheKey = `${tableName}:__introspected__`;
81
+ if (this.knownColumns.has(cacheKey))
82
+ return;
83
+ this.knownColumns.add(cacheKey);
84
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
85
+ try {
86
+ const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
87
+ if (Array.isArray(tableInfo)) {
88
+ for (const col of tableInfo) {
89
+ if (col && col.name) {
90
+ this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
91
+ }
92
+ }
93
+ }
94
+ // Auto-create high performance single and composite indexes for foreign keys (_id) and soft-deletes (deleted)
95
+ const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
96
+ for (const entry of this.knownColumns) {
97
+ if (!entry.startsWith(`${tableName}:`))
98
+ continue;
99
+ const colName = entry.slice(tableName.length + 1);
100
+ if (colName.endsWith('_id') && colName !== 'id') {
101
+ const fkIndexKey = `${tableName}:${colName}`;
102
+ if (!this.knownIndexes.has(fkIndexKey)) {
103
+ this.knownIndexes.add(fkIndexKey);
104
+ try {
105
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}`);
106
+ const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
107
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk});`);
108
+ }
109
+ catch { }
110
+ }
111
+ if (hasDeleted) {
112
+ const compositeKey = `${tableName}:${colName}_deleted`;
113
+ if (!this.knownIndexes.has(compositeKey)) {
114
+ this.knownIndexes.add(compositeKey);
115
+ try {
116
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}_del`);
117
+ const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
118
+ const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
119
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
120
+ }
121
+ catch { }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ catch {
128
+ // Ignore
129
+ }
67
130
  }
68
131
  /**
69
132
  * Ensures that the target table exists in SQLite with the specified primary key.
@@ -137,6 +200,46 @@ class SchemaManager {
137
200
  }
138
201
  this.knownColumns.add(cacheKey);
139
202
  }
203
+ // Auto-index foreign keys and common query columns
204
+ const lowerKey = key.toLowerCase();
205
+ if (lowerKey.endsWith('_id') ||
206
+ lowerKey === 'deleted' ||
207
+ lowerKey === 'created_at' ||
208
+ lowerKey === 'updated_at') {
209
+ const indexKey = `${tableName}:${lowerKey}`;
210
+ if (!this.knownIndexes.has(indexKey)) {
211
+ this.knownIndexes.add(indexKey);
212
+ try {
213
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`);
214
+ const safeCol = (0, sql_js_1.escapeIdentifier)(key);
215
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeCol});`);
216
+ }
217
+ catch {
218
+ // Ignore if indexes are not supported by adapter or already exist
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Auto-create high performance composite index for foreign keys with soft-deletion
225
+ if (this.knownColumns.has(`${tableName}:deleted`)) {
226
+ for (const key of keys) {
227
+ const lowerKey = key.toLowerCase();
228
+ if (lowerKey.endsWith('_id') && lowerKey !== 'id') {
229
+ const compositeKey = `${tableName}:${lowerKey}_deleted`;
230
+ if (!this.knownIndexes.has(compositeKey)) {
231
+ this.knownIndexes.add(compositeKey);
232
+ try {
233
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lowerKey}_del`);
234
+ const safeFk = (0, sql_js_1.escapeIdentifier)(key);
235
+ const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
236
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
237
+ }
238
+ catch {
239
+ // Ignore
240
+ }
241
+ }
242
+ }
140
243
  }
141
244
  }
142
245
  // Format all values for SQLite storage
@@ -16,6 +16,9 @@ export interface SyncWorkerOptions {
16
16
  primaryKey?: string;
17
17
  autoSchema?: boolean;
18
18
  schemaManager?: SchemaManager;
19
+ autoSync?: boolean;
20
+ syncScope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>);
21
+ autoPauseOnBackground?: boolean;
19
22
  }
20
23
  export declare class SyncWorker {
21
24
  private db;
@@ -26,20 +29,29 @@ export declare class SyncWorker {
26
29
  private primaryKey;
27
30
  private autoSchema;
28
31
  private schemaManager?;
32
+ private syncScope?;
29
33
  logger: SyncLogger;
30
34
  retryConfig: RetryConfig;
31
35
  private pushIntervalMs;
32
36
  private pullIntervalMs;
33
37
  private maxRetries;
38
+ private autoPauseOnBackground;
39
+ private isPaused;
40
+ private lifecycleUnsubscribe?;
34
41
  private pushIntervalTimer?;
35
42
  private pullIntervalTimer?;
36
43
  private pushDebounceTimer?;
37
44
  private networkUnsubscribe?;
45
+ private remoteUnsubscribe?;
46
+ private subscriptionGeneration;
47
+ private automaticSync;
38
48
  private isPushing;
39
49
  private isPulling;
40
50
  private lastPulledTimestamp;
41
51
  private cursorLoaded;
42
52
  private deltaQueue;
53
+ private statusRun?;
54
+ private statusRerunRequested;
43
55
  private totalPushedOperations;
44
56
  private totalPulledDeltas;
45
57
  private totalPushCycles;
@@ -59,11 +71,34 @@ export declare class SyncWorker {
59
71
  * Stops all active intervals, timers, and listeners.
60
72
  */
61
73
  stop(): void;
74
+ /**
75
+ * Pauses recurring interval timers and active sync loops to conserve battery and data.
76
+ */
77
+ pause(): void;
78
+ /**
79
+ * Resumes recurring interval timers and triggers an immediate catch-up push and pull.
80
+ */
81
+ resume(): void;
82
+ /**
83
+ * Returns whether the sync worker is currently suspended due to app backgrounding.
84
+ */
85
+ get isBackgroundPaused(): boolean;
86
+ private isAppBackgrounded;
87
+ private setupLifecycleListener;
62
88
  /**
63
89
  * Schedules an immediate push with a short debounce.
64
90
  */
65
91
  schedulePush(delayMs?: number): void;
92
+ /**
93
+ * Dynamically sets or updates the active sync scope (e.g. switching active businessId or userId).
94
+ */
95
+ setSyncScope(scope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>)): void;
66
96
  private isOnline;
97
+ /**
98
+ * Emits a `sync:status` snapshot. Calls are coalesced: while one snapshot is being
99
+ * computed, further requests collapse into a single trailing run that reflects the
100
+ * latest state, so bursts of push/pull transitions cost at most two COUNT queries.
101
+ */
67
102
  private emitStatus;
68
103
  /**
69
104
  * Pushes pending outbox records to the remote database.
@@ -78,7 +113,9 @@ export declare class SyncWorker {
78
113
  * Applies incoming server delta changes to local tables with conflict resolution.
79
114
  * Serializes execution in a queue to prevent concurrent SQLite transactions.
80
115
  */
81
- applyDeltasLocally(deltas: DeltaChanges): Promise<void>;
116
+ applyDeltasLocally(deltas: DeltaChanges, options?: {
117
+ advanceCursor?: boolean;
118
+ }): Promise<void>;
82
119
  private processDeltasLocally;
83
120
  getLastPulledTimestamp(): Promise<number>;
84
121
  resetSyncCursor(): Promise<void>;