local-lite 0.0.2 → 0.0.4
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/README.md +421 -362
- package/dist/adapters/local/better-sqlite3.d.ts +2 -0
- package/dist/adapters/local/better-sqlite3.js +46 -25
- package/dist/adapters/local/expo-sqlite.d.ts +5 -1
- package/dist/adapters/local/expo-sqlite.js +40 -7
- package/dist/adapters/local/index.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +2 -0
- package/dist/adapters/local/node-sqlite.js +45 -22
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +6 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +5 -0
- package/dist/adapters/network/index.js +33 -0
- package/dist/adapters/remote/convex.d.ts +2 -2
- package/dist/adapters/remote/convex.js +8 -12
- package/dist/adapters/remote/demo.d.ts +1 -1
- package/dist/adapters/remote/demo.js +3 -0
- package/dist/adapters/remote/firestore.d.ts +3 -2
- package/dist/adapters/remote/firestore.js +69 -46
- package/dist/adapters/remote/index.d.ts +6 -4
- package/dist/adapters/remote/supabase.d.ts +34 -6
- package/dist/adapters/remote/supabase.js +340 -79
- package/dist/config/index.js +7 -10
- package/dist/config/resolve-local.js +12 -1
- package/dist/config/resolve-remote.js +33 -28
- package/dist/config/types.d.ts +30 -5
- package/dist/core/client.d.ts +86 -3
- package/dist/core/client.js +423 -41
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +29 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query.d.ts +8 -0
- package/dist/core/query.js +37 -0
- package/dist/core/schema-manager.d.ts +9 -0
- package/dist/core/schema-manager.js +68 -0
- package/dist/core/sync-worker.d.ts +38 -1
- package/dist/core/sync-worker.js +351 -104
- package/dist/core/table-manager.js +26 -29
- package/dist/core/types.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/react/BenchmarkModal.d.ts +32 -0
- package/dist/react/BenchmarkModal.js +777 -0
- package/dist/react/SyncInspector.d.ts +31 -0
- package/dist/react/SyncInspector.js +655 -0
- package/dist/react/index.d.ts +49 -19
- package/dist/react/index.js +266 -95
- package/dist/react/runner.d.ts +7 -0
- package/dist/react/runner.js +42 -0
- package/dist/react/status.d.ts +24 -0
- package/dist/react/status.js +123 -0
- package/dist/react/theme.d.ts +18 -0
- package/dist/react/theme.js +46 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +138 -133
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OutboxStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Maps a raw sync_outbox row into an OutboxRecord.
|
|
6
|
+
*/
|
|
7
|
+
function mapOutboxRow(r) {
|
|
8
|
+
return {
|
|
9
|
+
id: r.id,
|
|
10
|
+
tableName: r.table_name,
|
|
11
|
+
rowId: r.row_id,
|
|
12
|
+
operation: r.operation,
|
|
13
|
+
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
14
|
+
timestamp: Number(r.timestamp),
|
|
15
|
+
status: r.status,
|
|
16
|
+
retryCount: Number(r.retry_count || 0),
|
|
17
|
+
lastError: r.last_error,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
4
20
|
class OutboxStore {
|
|
5
21
|
/**
|
|
6
22
|
* Initializes the sync_outbox schema if it does not already exist.
|
|
@@ -17,6 +33,10 @@ class OutboxStore {
|
|
|
17
33
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
18
34
|
last_error TEXT
|
|
19
35
|
);`);
|
|
36
|
+
// Indexes for the hot paths: pending scans (push), per-row conflict lookups (pull),
|
|
37
|
+
// and status counts (telemetry). Executed separately: many drivers prepare one statement.
|
|
38
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_status_ts ON sync_outbox (status, timestamp);`);
|
|
39
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_row ON sync_outbox (table_name, row_id, timestamp);`);
|
|
20
40
|
}
|
|
21
41
|
/**
|
|
22
42
|
* Inserts a new outbox entry within an existing database transaction.
|
|
@@ -33,11 +53,29 @@ class OutboxStore {
|
|
|
33
53
|
]);
|
|
34
54
|
}
|
|
35
55
|
/**
|
|
36
|
-
* Inserts multiple outbox entries within an existing
|
|
56
|
+
* Inserts multiple outbox entries in high-throughput multi-row SQL chunks within an existing transaction.
|
|
37
57
|
*/
|
|
38
58
|
static async insertRecords(tx, ops) {
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
if (!ops || ops.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
const CHUNK_SIZE = 50;
|
|
62
|
+
for (let i = 0; i < ops.length; i += CHUNK_SIZE) {
|
|
63
|
+
const chunk = ops.slice(i, i + CHUNK_SIZE);
|
|
64
|
+
const valueClauses = [];
|
|
65
|
+
const params = [];
|
|
66
|
+
for (const op of chunk) {
|
|
67
|
+
valueClauses.push('(?, ?, ?, ?, ?, ?, "PENDING", 0)');
|
|
68
|
+
params.push(op.id, op.tableName, op.rowId, op.operation, op.payload ? JSON.stringify(op.payload) : null, op.timestamp);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await tx.execute(`INSERT INTO sync_outbox (id, table_name, row_id, operation, payload, timestamp, status, retry_count) VALUES ${valueClauses.join(', ')}`, params);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Transparent fallback to per-record insert for mock drivers or custom engines
|
|
75
|
+
for (const op of chunk) {
|
|
76
|
+
await this.insertRecord(tx, op);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
/**
|
|
@@ -56,17 +94,7 @@ class OutboxStore {
|
|
|
56
94
|
const rows = await db.execute(query, params);
|
|
57
95
|
if (!rows || rows.length === 0)
|
|
58
96
|
return [];
|
|
59
|
-
return rows.map(
|
|
60
|
-
id: r.id,
|
|
61
|
-
tableName: r.table_name,
|
|
62
|
-
rowId: r.row_id,
|
|
63
|
-
operation: r.operation,
|
|
64
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
65
|
-
timestamp: Number(r.timestamp),
|
|
66
|
-
status: r.status,
|
|
67
|
-
retryCount: Number(r.retry_count || 0),
|
|
68
|
-
lastError: r.last_error,
|
|
69
|
-
}));
|
|
97
|
+
return rows.map(mapOutboxRow);
|
|
70
98
|
}
|
|
71
99
|
/**
|
|
72
100
|
* Finds the latest pending outbox record for a given table and row.
|
|
@@ -77,18 +105,7 @@ class OutboxStore {
|
|
|
77
105
|
ORDER BY timestamp DESC LIMIT 1`, [tableName, rowId]);
|
|
78
106
|
if (!rows || rows.length === 0)
|
|
79
107
|
return undefined;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
id: r.id,
|
|
83
|
-
tableName: r.table_name,
|
|
84
|
-
rowId: r.row_id,
|
|
85
|
-
operation: r.operation,
|
|
86
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
87
|
-
timestamp: Number(r.timestamp),
|
|
88
|
-
status: r.status,
|
|
89
|
-
retryCount: Number(r.retry_count || 0),
|
|
90
|
-
lastError: r.last_error,
|
|
91
|
-
};
|
|
108
|
+
return mapOutboxRow(rows[0]);
|
|
92
109
|
}
|
|
93
110
|
/**
|
|
94
111
|
* Marks a batch of outbox IDs as 'PROCESSING'.
|
|
@@ -127,10 +144,22 @@ class OutboxStore {
|
|
|
127
144
|
await tx.execute(`DELETE FROM sync_outbox WHERE id = ?`, [id]);
|
|
128
145
|
}
|
|
129
146
|
/**
|
|
130
|
-
* Updates an outbox record's payload (used during conflict merge).
|
|
147
|
+
* Updates an outbox record's payload (used during conflict merge or coalescing).
|
|
131
148
|
*/
|
|
132
|
-
static async updatePayload(tx, id, payload) {
|
|
133
|
-
|
|
149
|
+
static async updatePayload(tx, id, payload, timestamp) {
|
|
150
|
+
if (timestamp) {
|
|
151
|
+
await tx.execute(`UPDATE sync_outbox SET payload = ?, timestamp = ? WHERE id = ?`, [JSON.stringify(payload), timestamp, id]);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
await tx.execute(`UPDATE sync_outbox SET payload = ? WHERE id = ?`, [JSON.stringify(payload), id]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Updates an outbox record's operation and payload (used during mutation coalescing).
|
|
159
|
+
*/
|
|
160
|
+
static async updateOperation(tx, id, operation, payload, timestamp) {
|
|
161
|
+
const ts = timestamp ?? Date.now();
|
|
162
|
+
await tx.execute(`UPDATE sync_outbox SET operation = ?, payload = ?, timestamp = ? WHERE id = ?`, [operation, payload ? JSON.stringify(payload) : null, ts, id]);
|
|
134
163
|
}
|
|
135
164
|
/**
|
|
136
165
|
* Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
|
|
@@ -150,27 +179,23 @@ class OutboxStore {
|
|
|
150
179
|
*/
|
|
151
180
|
static async getCounts(db) {
|
|
152
181
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return { pending: 0, failed: 0, processing: 0, total: 0 };
|
|
156
|
-
}
|
|
182
|
+
// Aggregate in SQLite (index-only scan) instead of loading every row and payload into JS.
|
|
183
|
+
const rows = await db.execute(`SELECT status, COUNT(*) AS count FROM sync_outbox GROUP BY status`);
|
|
157
184
|
let pending = 0;
|
|
158
185
|
let failed = 0;
|
|
159
186
|
let processing = 0;
|
|
160
|
-
|
|
187
|
+
let total = 0;
|
|
188
|
+
for (const row of rows || []) {
|
|
189
|
+
const count = Number(row.count ?? 0);
|
|
190
|
+
total += count;
|
|
161
191
|
if (row.status === 'PENDING')
|
|
162
|
-
pending
|
|
192
|
+
pending = count;
|
|
163
193
|
else if (row.status === 'FAILED')
|
|
164
|
-
failed
|
|
194
|
+
failed = count;
|
|
165
195
|
else if (row.status === 'PROCESSING')
|
|
166
|
-
processing
|
|
196
|
+
processing = count;
|
|
167
197
|
}
|
|
168
|
-
return {
|
|
169
|
-
pending,
|
|
170
|
-
failed,
|
|
171
|
-
processing,
|
|
172
|
-
total: rows.length,
|
|
173
|
-
};
|
|
198
|
+
return { pending, failed, processing, total };
|
|
174
199
|
}
|
|
175
200
|
catch {
|
|
176
201
|
return { pending: 0, failed: 0, processing: 0, total: 0 };
|
|
@@ -231,31 +256,25 @@ class OutboxStore {
|
|
|
231
256
|
*/
|
|
232
257
|
static async getAllRecords(db, options) {
|
|
233
258
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let mapped = rows.map((r) => ({
|
|
238
|
-
id: r.id,
|
|
239
|
-
tableName: r.table_name,
|
|
240
|
-
rowId: r.row_id,
|
|
241
|
-
operation: r.operation,
|
|
242
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
243
|
-
timestamp: Number(r.timestamp),
|
|
244
|
-
status: r.status,
|
|
245
|
-
retryCount: Number(r.retry_count || 0),
|
|
246
|
-
lastError: r.last_error,
|
|
247
|
-
}));
|
|
259
|
+
// Filter, order and paginate in SQLite so only the requested page is parsed.
|
|
260
|
+
const params = [];
|
|
261
|
+
let query = `SELECT * FROM sync_outbox`;
|
|
248
262
|
if (options?.status) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
|
252
|
-
if (options?.offset && options.offset > 0) {
|
|
253
|
-
mapped = mapped.slice(options.offset);
|
|
263
|
+
query += ` WHERE status = ?`;
|
|
264
|
+
params.push(options.status);
|
|
254
265
|
}
|
|
255
|
-
|
|
256
|
-
|
|
266
|
+
query += ` ORDER BY timestamp DESC`;
|
|
267
|
+
const limit = options?.limit && options.limit > 0 ? options.limit : -1;
|
|
268
|
+
const offset = options?.offset && options.offset > 0 ? options.offset : 0;
|
|
269
|
+
if (limit > 0 || offset > 0) {
|
|
270
|
+
// SQLite requires LIMIT when OFFSET is used; -1 means "no limit".
|
|
271
|
+
query += ` LIMIT ? OFFSET ?`;
|
|
272
|
+
params.push(limit, offset);
|
|
257
273
|
}
|
|
258
|
-
|
|
274
|
+
const rows = await db.execute(query, params);
|
|
275
|
+
if (!rows || rows.length === 0)
|
|
276
|
+
return [];
|
|
277
|
+
return rows.map(mapOutboxRow);
|
|
259
278
|
}
|
|
260
279
|
catch {
|
|
261
280
|
return [];
|
package/dist/core/query.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/core/query.js
CHANGED
|
@@ -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,43 @@ 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
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
/**
|
|
69
100
|
* Ensures that the target table exists in SQLite with the specified primary key.
|
|
@@ -137,6 +168,43 @@ class SchemaManager {
|
|
|
137
168
|
}
|
|
138
169
|
this.knownColumns.add(cacheKey);
|
|
139
170
|
}
|
|
171
|
+
// Auto-index foreign keys and common query columns
|
|
172
|
+
const lowerKey = key.toLowerCase();
|
|
173
|
+
if (lowerKey.endsWith('_id') ||
|
|
174
|
+
lowerKey === 'business_id' ||
|
|
175
|
+
lowerKey === 'deleted' ||
|
|
176
|
+
lowerKey === 'created_at' ||
|
|
177
|
+
lowerKey === 'payment_date') {
|
|
178
|
+
const indexKey = `${tableName}:${lowerKey}`;
|
|
179
|
+
if (!this.knownIndexes.has(indexKey)) {
|
|
180
|
+
this.knownIndexes.add(indexKey);
|
|
181
|
+
try {
|
|
182
|
+
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`);
|
|
183
|
+
const safeCol = (0, sql_js_1.escapeIdentifier)(key);
|
|
184
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeCol});`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Ignore if indexes are not supported by adapter or already exist
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Auto-create high performance composite index for multi-tenant soft-deleted queries
|
|
194
|
+
if (this.knownColumns.has(`${tableName}:business_id`) &&
|
|
195
|
+
this.knownColumns.has(`${tableName}:deleted`)) {
|
|
196
|
+
const compositeKey = `${tableName}:business_id_deleted`;
|
|
197
|
+
if (!this.knownIndexes.has(compositeKey)) {
|
|
198
|
+
this.knownIndexes.add(compositeKey);
|
|
199
|
+
try {
|
|
200
|
+
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_biz_del`);
|
|
201
|
+
const safeBiz = (0, sql_js_1.escapeIdentifier)('business_id');
|
|
202
|
+
const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
|
|
203
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeBiz}, ${safeDel});`);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Ignore
|
|
207
|
+
}
|
|
140
208
|
}
|
|
141
209
|
}
|
|
142
210
|
// 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
|
|
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>;
|