@hasna/cloud 0.1.2 → 0.1.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/dist/auto-sync.d.ts +57 -0
- package/dist/auto-sync.d.ts.map +1 -0
- package/dist/cli/index.js +170 -154
- package/dist/cli-helpers.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +723 -63
- package/dist/mcp/index.js +167 -151
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/sync-conflicts.d.ts +76 -0
- package/dist/sync-conflicts.d.ts.map +1 -0
- package/dist/sync-incremental.d.ts +62 -0
- package/dist/sync-incremental.d.ts.map +1 -0
- package/dist/sync-progress.d.ts +68 -0
- package/dist/sync-progress.d.ts.map +1 -0
- package/dist/sync.d.ts +10 -9
- package/dist/sync.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -155,7 +155,7 @@ var require_arrayParser = __commonJS((exports, module) => {
|
|
|
155
155
|
};
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
// node_modules/postgres-date/index.js
|
|
158
|
+
// node_modules/pg-types/node_modules/postgres-date/index.js
|
|
159
159
|
var require_postgres_date = __commonJS((exports, module) => {
|
|
160
160
|
var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
|
|
161
161
|
var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
|
|
@@ -257,7 +257,7 @@ var require_mutable = __commonJS((exports, module) => {
|
|
|
257
257
|
}
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
-
// node_modules/postgres-interval/index.js
|
|
260
|
+
// node_modules/pg-types/node_modules/postgres-interval/index.js
|
|
261
261
|
var require_postgres_interval = __commonJS((exports, module) => {
|
|
262
262
|
var extend = require_mutable();
|
|
263
263
|
module.exports = PostgresInterval;
|
|
@@ -349,7 +349,7 @@ var require_postgres_interval = __commonJS((exports, module) => {
|
|
|
349
349
|
}
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
// node_modules/postgres-bytea/index.js
|
|
352
|
+
// node_modules/pg-types/node_modules/postgres-bytea/index.js
|
|
353
353
|
var require_postgres_bytea = __commonJS((exports, module) => {
|
|
354
354
|
var bufferFrom = Buffer.from || Buffer;
|
|
355
355
|
module.exports = function parseBytea(input) {
|
|
@@ -9309,17 +9309,88 @@ function createDatabase(options) {
|
|
|
9309
9309
|
return new SqliteAdapter(dbPath);
|
|
9310
9310
|
}
|
|
9311
9311
|
// src/sync.ts
|
|
9312
|
-
function syncPush(local,
|
|
9313
|
-
|
|
9312
|
+
async function syncPush(local, remote, options) {
|
|
9313
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
9314
|
+
return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
|
|
9314
9315
|
}
|
|
9315
|
-
function syncPull(
|
|
9316
|
-
|
|
9316
|
+
async function syncPull(remote, local, options) {
|
|
9317
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
9318
|
+
return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
|
|
9317
9319
|
}
|
|
9318
|
-
function
|
|
9320
|
+
async function getTableOrder(remote, tables) {
|
|
9321
|
+
if (tables.length <= 1)
|
|
9322
|
+
return tables;
|
|
9323
|
+
try {
|
|
9324
|
+
const fks = await remote.all(`
|
|
9325
|
+
SELECT DISTINCT
|
|
9326
|
+
tc.table_name AS source_table,
|
|
9327
|
+
ccu.table_name AS referenced_table
|
|
9328
|
+
FROM information_schema.table_constraints tc
|
|
9329
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
9330
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
9331
|
+
AND tc.table_schema = ccu.table_schema
|
|
9332
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
9333
|
+
AND tc.table_schema = 'public'
|
|
9334
|
+
`);
|
|
9335
|
+
if (fks.length > 0) {
|
|
9336
|
+
return topoSort(tables, fks);
|
|
9337
|
+
}
|
|
9338
|
+
} catch {}
|
|
9339
|
+
return heuristicOrder(tables);
|
|
9340
|
+
}
|
|
9341
|
+
function topoSort(tables, fks) {
|
|
9342
|
+
const tableSet = new Set(tables);
|
|
9343
|
+
const deps = new Map;
|
|
9344
|
+
for (const t of tables) {
|
|
9345
|
+
deps.set(t, new Set);
|
|
9346
|
+
}
|
|
9347
|
+
for (const fk of fks) {
|
|
9348
|
+
if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
|
|
9349
|
+
deps.get(fk.source_table).add(fk.referenced_table);
|
|
9350
|
+
}
|
|
9351
|
+
}
|
|
9352
|
+
const sorted = [];
|
|
9353
|
+
const visited = new Set;
|
|
9354
|
+
const visiting = new Set;
|
|
9355
|
+
function visit(table) {
|
|
9356
|
+
if (visited.has(table))
|
|
9357
|
+
return;
|
|
9358
|
+
if (visiting.has(table)) {
|
|
9359
|
+
sorted.push(table);
|
|
9360
|
+
visited.add(table);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9363
|
+
visiting.add(table);
|
|
9364
|
+
const tableDeps = deps.get(table) ?? new Set;
|
|
9365
|
+
for (const dep of tableDeps) {
|
|
9366
|
+
visit(dep);
|
|
9367
|
+
}
|
|
9368
|
+
visiting.delete(table);
|
|
9369
|
+
visited.add(table);
|
|
9370
|
+
sorted.push(table);
|
|
9371
|
+
}
|
|
9372
|
+
for (const t of tables) {
|
|
9373
|
+
visit(t);
|
|
9374
|
+
}
|
|
9375
|
+
return sorted;
|
|
9376
|
+
}
|
|
9377
|
+
function heuristicOrder(tables) {
|
|
9378
|
+
const sorted = [...tables].sort((a, b) => {
|
|
9379
|
+
const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
|
|
9380
|
+
const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
|
|
9381
|
+
if (aIsChild && !bIsChild)
|
|
9382
|
+
return 1;
|
|
9383
|
+
if (!aIsChild && bIsChild)
|
|
9384
|
+
return -1;
|
|
9385
|
+
return a.localeCompare(b);
|
|
9386
|
+
});
|
|
9387
|
+
return sorted;
|
|
9388
|
+
}
|
|
9389
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
9319
9390
|
const {
|
|
9320
9391
|
tables,
|
|
9321
9392
|
onProgress,
|
|
9322
|
-
batchSize =
|
|
9393
|
+
batchSize = 100,
|
|
9323
9394
|
conflictColumn = "updated_at",
|
|
9324
9395
|
primaryKey = "id"
|
|
9325
9396
|
} = options;
|
|
@@ -9342,7 +9413,7 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
9342
9413
|
totalTables: tables.length,
|
|
9343
9414
|
currentTableIndex: i
|
|
9344
9415
|
});
|
|
9345
|
-
const rows = source
|
|
9416
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
9346
9417
|
result.rowsRead = rows.length;
|
|
9347
9418
|
if (rows.length === 0) {
|
|
9348
9419
|
onProgress?.({
|
|
@@ -9357,7 +9428,6 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
9357
9428
|
continue;
|
|
9358
9429
|
}
|
|
9359
9430
|
const columns = Object.keys(rows[0]);
|
|
9360
|
-
const hasConflictCol = columns.includes(conflictColumn);
|
|
9361
9431
|
const hasPrimaryKey = columns.includes(primaryKey);
|
|
9362
9432
|
if (!hasPrimaryKey) {
|
|
9363
9433
|
result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
|
|
@@ -9372,34 +9442,18 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
9372
9442
|
totalTables: tables.length,
|
|
9373
9443
|
currentTableIndex: i
|
|
9374
9444
|
});
|
|
9445
|
+
const updateCols = columns.filter((c) => c !== primaryKey);
|
|
9375
9446
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
9376
9447
|
const batch = rows.slice(offset, offset + batchSize);
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
9383
|
-
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
9384
|
-
if (existingTime >= incomingTime) {
|
|
9385
|
-
result.rowsSkipped++;
|
|
9386
|
-
continue;
|
|
9387
|
-
}
|
|
9388
|
-
}
|
|
9389
|
-
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
9390
|
-
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
9391
|
-
values.push(row[primaryKey]);
|
|
9392
|
-
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
9393
|
-
} else {
|
|
9394
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
9395
|
-
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9396
|
-
const values = columns.map((c) => row[c]);
|
|
9397
|
-
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
9398
|
-
}
|
|
9399
|
-
result.rowsWritten++;
|
|
9400
|
-
} catch (err) {
|
|
9401
|
-
result.errors.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
9448
|
+
try {
|
|
9449
|
+
if (isAsyncAdapter(target)) {
|
|
9450
|
+
await batchUpsertPg(target, table, columns, updateCols, primaryKey, batch);
|
|
9451
|
+
} else {
|
|
9452
|
+
batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
|
|
9402
9453
|
}
|
|
9454
|
+
result.rowsWritten += batch.length;
|
|
9455
|
+
} catch (err) {
|
|
9456
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
9403
9457
|
}
|
|
9404
9458
|
onProgress?.({
|
|
9405
9459
|
table,
|
|
@@ -9425,12 +9479,44 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
9425
9479
|
}
|
|
9426
9480
|
return results;
|
|
9427
9481
|
}
|
|
9482
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKey, batch) {
|
|
9483
|
+
if (batch.length === 0)
|
|
9484
|
+
return;
|
|
9485
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9486
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
9487
|
+
const offset = rowIdx * columns.length;
|
|
9488
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
9489
|
+
}).join(", ");
|
|
9490
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
9491
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
9492
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
9493
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9494
|
+
await target.run(sql, ...params);
|
|
9495
|
+
}
|
|
9496
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch) {
|
|
9497
|
+
if (batch.length === 0)
|
|
9498
|
+
return;
|
|
9499
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9500
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
9501
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
9502
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
9503
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
9504
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9505
|
+
target.run(sql, ...params);
|
|
9506
|
+
}
|
|
9507
|
+
function isAsyncAdapter(adapter) {
|
|
9508
|
+
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
9509
|
+
}
|
|
9510
|
+
async function readAll(adapter, sql) {
|
|
9511
|
+
const result = adapter.all(sql);
|
|
9512
|
+
return result instanceof Promise ? await result : result;
|
|
9513
|
+
}
|
|
9428
9514
|
function listSqliteTables(db) {
|
|
9429
9515
|
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
9430
9516
|
return rows.map((r) => r.name);
|
|
9431
9517
|
}
|
|
9432
|
-
function listPgTables(db) {
|
|
9433
|
-
const rows = db.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`);
|
|
9518
|
+
async function listPgTables(db) {
|
|
9519
|
+
const rows = await db.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`);
|
|
9434
9520
|
return rows.map((r) => r.tablename);
|
|
9435
9521
|
}
|
|
9436
9522
|
// src/feedback.ts
|
|
@@ -9501,6 +9587,562 @@ function listFeedback(db) {
|
|
|
9501
9587
|
ensureFeedbackTable(db);
|
|
9502
9588
|
return db.all(`SELECT id, service, version, message, email, machine_id, created_at FROM feedback ORDER BY created_at DESC`);
|
|
9503
9589
|
}
|
|
9590
|
+
// src/sync-progress.ts
|
|
9591
|
+
class SyncProgressTracker {
|
|
9592
|
+
db;
|
|
9593
|
+
progress = new Map;
|
|
9594
|
+
startTimes = new Map;
|
|
9595
|
+
callback;
|
|
9596
|
+
constructor(db, callback) {
|
|
9597
|
+
this.db = db;
|
|
9598
|
+
this.callback = callback;
|
|
9599
|
+
this.ensureResumeTable();
|
|
9600
|
+
}
|
|
9601
|
+
ensureResumeTable() {
|
|
9602
|
+
this.db.exec(`
|
|
9603
|
+
CREATE TABLE IF NOT EXISTS _sync_resume (
|
|
9604
|
+
table_name TEXT PRIMARY KEY,
|
|
9605
|
+
last_row_id TEXT,
|
|
9606
|
+
direction TEXT,
|
|
9607
|
+
started_at TEXT,
|
|
9608
|
+
status TEXT DEFAULT 'in_progress'
|
|
9609
|
+
)
|
|
9610
|
+
`);
|
|
9611
|
+
}
|
|
9612
|
+
start(table, total, direction) {
|
|
9613
|
+
const resumed = this.canResume(table);
|
|
9614
|
+
const now = Date.now();
|
|
9615
|
+
this.startTimes.set(table, now);
|
|
9616
|
+
const status = resumed ? "resumed" : "in_progress";
|
|
9617
|
+
const info = {
|
|
9618
|
+
table,
|
|
9619
|
+
total,
|
|
9620
|
+
done: 0,
|
|
9621
|
+
percent: 0,
|
|
9622
|
+
elapsed_ms: 0,
|
|
9623
|
+
eta_ms: 0,
|
|
9624
|
+
status
|
|
9625
|
+
};
|
|
9626
|
+
this.progress.set(table, info);
|
|
9627
|
+
this.db.run(`INSERT INTO _sync_resume (table_name, last_row_id, direction, started_at, status)
|
|
9628
|
+
VALUES (?, ?, ?, datetime('now'), ?)
|
|
9629
|
+
ON CONFLICT (table_name) DO UPDATE SET
|
|
9630
|
+
direction = excluded.direction,
|
|
9631
|
+
started_at = datetime('now'),
|
|
9632
|
+
status = excluded.status`, table, "", direction, status);
|
|
9633
|
+
this.notify(table);
|
|
9634
|
+
}
|
|
9635
|
+
update(table, done, lastRowId) {
|
|
9636
|
+
const info = this.progress.get(table);
|
|
9637
|
+
if (!info)
|
|
9638
|
+
return;
|
|
9639
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
9640
|
+
const elapsed = Date.now() - startTime;
|
|
9641
|
+
const rate = done > 0 ? elapsed / done : 0;
|
|
9642
|
+
const remaining = info.total - done;
|
|
9643
|
+
const eta = remaining > 0 ? Math.round(rate * remaining) : 0;
|
|
9644
|
+
info.done = done;
|
|
9645
|
+
info.percent = info.total > 0 ? Math.round(done / info.total * 100) : 0;
|
|
9646
|
+
info.elapsed_ms = elapsed;
|
|
9647
|
+
info.eta_ms = eta;
|
|
9648
|
+
info.status = "in_progress";
|
|
9649
|
+
this.db.run(`UPDATE _sync_resume SET last_row_id = ?, status = 'in_progress' WHERE table_name = ?`, lastRowId, table);
|
|
9650
|
+
this.notify(table);
|
|
9651
|
+
}
|
|
9652
|
+
markComplete(table) {
|
|
9653
|
+
const info = this.progress.get(table);
|
|
9654
|
+
if (info) {
|
|
9655
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
9656
|
+
info.elapsed_ms = Date.now() - startTime;
|
|
9657
|
+
info.done = info.total;
|
|
9658
|
+
info.percent = 100;
|
|
9659
|
+
info.eta_ms = 0;
|
|
9660
|
+
info.status = "completed";
|
|
9661
|
+
this.notify(table);
|
|
9662
|
+
}
|
|
9663
|
+
this.db.run(`UPDATE _sync_resume SET status = 'completed' WHERE table_name = ?`, table);
|
|
9664
|
+
}
|
|
9665
|
+
markFailed(table, _error) {
|
|
9666
|
+
const info = this.progress.get(table);
|
|
9667
|
+
if (info) {
|
|
9668
|
+
const startTime = this.startTimes.get(table) ?? Date.now();
|
|
9669
|
+
info.elapsed_ms = Date.now() - startTime;
|
|
9670
|
+
info.status = "failed";
|
|
9671
|
+
this.notify(table);
|
|
9672
|
+
}
|
|
9673
|
+
this.db.run(`UPDATE _sync_resume SET status = 'failed' WHERE table_name = ?`, table);
|
|
9674
|
+
}
|
|
9675
|
+
canResume(table) {
|
|
9676
|
+
const row = this.db.get(`SELECT status FROM _sync_resume WHERE table_name = ?`, table);
|
|
9677
|
+
if (!row)
|
|
9678
|
+
return false;
|
|
9679
|
+
return row.status === "in_progress" || row.status === "resumed";
|
|
9680
|
+
}
|
|
9681
|
+
getResumePoint(table) {
|
|
9682
|
+
const row = this.db.get(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume WHERE table_name = ?`, table);
|
|
9683
|
+
if (!row)
|
|
9684
|
+
return null;
|
|
9685
|
+
if (row.status !== "in_progress" && row.status !== "resumed")
|
|
9686
|
+
return null;
|
|
9687
|
+
return row;
|
|
9688
|
+
}
|
|
9689
|
+
clearResume(table) {
|
|
9690
|
+
this.db.run(`DELETE FROM _sync_resume WHERE table_name = ?`, table);
|
|
9691
|
+
this.progress.delete(table);
|
|
9692
|
+
this.startTimes.delete(table);
|
|
9693
|
+
}
|
|
9694
|
+
getProgress(table) {
|
|
9695
|
+
return this.progress.get(table) ?? null;
|
|
9696
|
+
}
|
|
9697
|
+
getAllProgress() {
|
|
9698
|
+
return Array.from(this.progress.values());
|
|
9699
|
+
}
|
|
9700
|
+
listResumeRecords() {
|
|
9701
|
+
return this.db.all(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume ORDER BY started_at DESC`);
|
|
9702
|
+
}
|
|
9703
|
+
notify(table) {
|
|
9704
|
+
const info = this.progress.get(table);
|
|
9705
|
+
if (info && this.callback) {
|
|
9706
|
+
this.callback({ ...info });
|
|
9707
|
+
}
|
|
9708
|
+
}
|
|
9709
|
+
}
|
|
9710
|
+
// src/sync-conflicts.ts
|
|
9711
|
+
function detectConflicts(local, remote, table, primaryKey = "id", conflictColumn = "updated_at") {
|
|
9712
|
+
const conflicts = [];
|
|
9713
|
+
const remoteMap = new Map;
|
|
9714
|
+
for (const row of remote) {
|
|
9715
|
+
const key = String(row[primaryKey]);
|
|
9716
|
+
remoteMap.set(key, row);
|
|
9717
|
+
}
|
|
9718
|
+
for (const localRow of local) {
|
|
9719
|
+
const key = String(localRow[primaryKey]);
|
|
9720
|
+
const remoteRow = remoteMap.get(key);
|
|
9721
|
+
if (!remoteRow)
|
|
9722
|
+
continue;
|
|
9723
|
+
const localTs = localRow[conflictColumn];
|
|
9724
|
+
const remoteTs = remoteRow[conflictColumn];
|
|
9725
|
+
if (localTs !== remoteTs) {
|
|
9726
|
+
conflicts.push({
|
|
9727
|
+
table,
|
|
9728
|
+
row_id: key,
|
|
9729
|
+
local_updated_at: String(localTs ?? ""),
|
|
9730
|
+
remote_updated_at: String(remoteTs ?? ""),
|
|
9731
|
+
local_data: { ...localRow },
|
|
9732
|
+
remote_data: { ...remoteRow },
|
|
9733
|
+
resolved: false
|
|
9734
|
+
});
|
|
9735
|
+
}
|
|
9736
|
+
}
|
|
9737
|
+
return conflicts;
|
|
9738
|
+
}
|
|
9739
|
+
function resolveConflicts(conflicts, strategy = "newest-wins") {
|
|
9740
|
+
return conflicts.map((conflict) => {
|
|
9741
|
+
const resolved = { ...conflict, resolved: true, resolution: strategy };
|
|
9742
|
+
switch (strategy) {
|
|
9743
|
+
case "local-wins":
|
|
9744
|
+
break;
|
|
9745
|
+
case "remote-wins":
|
|
9746
|
+
break;
|
|
9747
|
+
case "newest-wins": {
|
|
9748
|
+
const localTime = new Date(conflict.local_updated_at).getTime();
|
|
9749
|
+
const remoteTime = new Date(conflict.remote_updated_at).getTime();
|
|
9750
|
+
if (remoteTime > localTime) {
|
|
9751
|
+
resolved.resolution = "newest-wins";
|
|
9752
|
+
} else {
|
|
9753
|
+
resolved.resolution = "newest-wins";
|
|
9754
|
+
}
|
|
9755
|
+
break;
|
|
9756
|
+
}
|
|
9757
|
+
}
|
|
9758
|
+
return resolved;
|
|
9759
|
+
});
|
|
9760
|
+
}
|
|
9761
|
+
function getWinningData(conflict) {
|
|
9762
|
+
if (!conflict.resolved || !conflict.resolution) {
|
|
9763
|
+
throw new Error(`Conflict for row ${conflict.row_id} is not resolved`);
|
|
9764
|
+
}
|
|
9765
|
+
switch (conflict.resolution) {
|
|
9766
|
+
case "local-wins":
|
|
9767
|
+
return conflict.local_data;
|
|
9768
|
+
case "remote-wins":
|
|
9769
|
+
return conflict.remote_data;
|
|
9770
|
+
case "newest-wins": {
|
|
9771
|
+
const localTime = new Date(conflict.local_updated_at).getTime();
|
|
9772
|
+
const remoteTime = new Date(conflict.remote_updated_at).getTime();
|
|
9773
|
+
return remoteTime >= localTime ? conflict.remote_data : conflict.local_data;
|
|
9774
|
+
}
|
|
9775
|
+
case "manual":
|
|
9776
|
+
return conflict.local_data;
|
|
9777
|
+
default:
|
|
9778
|
+
return conflict.local_data;
|
|
9779
|
+
}
|
|
9780
|
+
}
|
|
9781
|
+
function ensureConflictsTable(db) {
|
|
9782
|
+
db.exec(`
|
|
9783
|
+
CREATE TABLE IF NOT EXISTS _sync_conflicts (
|
|
9784
|
+
id TEXT PRIMARY KEY,
|
|
9785
|
+
table_name TEXT,
|
|
9786
|
+
row_id TEXT,
|
|
9787
|
+
local_data TEXT,
|
|
9788
|
+
remote_data TEXT,
|
|
9789
|
+
local_updated_at TEXT,
|
|
9790
|
+
remote_updated_at TEXT,
|
|
9791
|
+
resolution TEXT,
|
|
9792
|
+
resolved_at TEXT,
|
|
9793
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
9794
|
+
)
|
|
9795
|
+
`);
|
|
9796
|
+
}
|
|
9797
|
+
function storeConflicts(db, conflicts) {
|
|
9798
|
+
ensureConflictsTable(db);
|
|
9799
|
+
for (const conflict of conflicts) {
|
|
9800
|
+
const id = `${conflict.table}:${conflict.row_id}:${Date.now()}`;
|
|
9801
|
+
db.run(`INSERT INTO _sync_conflicts (id, table_name, row_id, local_data, remote_data, local_updated_at, remote_updated_at, resolution, resolved_at)
|
|
9802
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, conflict.table, conflict.row_id, JSON.stringify(conflict.local_data), JSON.stringify(conflict.remote_data), conflict.local_updated_at, conflict.remote_updated_at, conflict.resolution ?? null, conflict.resolved ? new Date().toISOString() : null);
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
function listConflicts(db, opts) {
|
|
9806
|
+
ensureConflictsTable(db);
|
|
9807
|
+
let sql = `SELECT * FROM _sync_conflicts WHERE 1=1`;
|
|
9808
|
+
const params = [];
|
|
9809
|
+
if (opts?.resolved !== undefined) {
|
|
9810
|
+
if (opts.resolved) {
|
|
9811
|
+
sql += ` AND resolution IS NOT NULL AND resolved_at IS NOT NULL`;
|
|
9812
|
+
} else {
|
|
9813
|
+
sql += ` AND (resolution IS NULL OR resolved_at IS NULL)`;
|
|
9814
|
+
}
|
|
9815
|
+
}
|
|
9816
|
+
if (opts?.table) {
|
|
9817
|
+
sql += ` AND table_name = ?`;
|
|
9818
|
+
params.push(opts.table);
|
|
9819
|
+
}
|
|
9820
|
+
sql += ` ORDER BY created_at DESC`;
|
|
9821
|
+
return db.all(sql, ...params);
|
|
9822
|
+
}
|
|
9823
|
+
function resolveConflict(db, conflictId, strategy) {
|
|
9824
|
+
ensureConflictsTable(db);
|
|
9825
|
+
const row = db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
9826
|
+
if (!row)
|
|
9827
|
+
return null;
|
|
9828
|
+
db.run(`UPDATE _sync_conflicts SET resolution = ?, resolved_at = datetime('now') WHERE id = ?`, strategy, conflictId);
|
|
9829
|
+
return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
9830
|
+
}
|
|
9831
|
+
function getConflict(db, conflictId) {
|
|
9832
|
+
ensureConflictsTable(db);
|
|
9833
|
+
return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
9834
|
+
}
|
|
9835
|
+
function purgeResolvedConflicts(db) {
|
|
9836
|
+
ensureConflictsTable(db);
|
|
9837
|
+
const result = db.run(`DELETE FROM _sync_conflicts WHERE resolution IS NOT NULL AND resolved_at IS NOT NULL`);
|
|
9838
|
+
return result.changes;
|
|
9839
|
+
}
|
|
9840
|
+
// src/sync-incremental.ts
|
|
9841
|
+
var SYNC_META_TABLE_SQL = `
|
|
9842
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
9843
|
+
table_name TEXT PRIMARY KEY,
|
|
9844
|
+
last_synced_at TEXT,
|
|
9845
|
+
last_synced_row_count INTEGER DEFAULT 0,
|
|
9846
|
+
direction TEXT DEFAULT 'push'
|
|
9847
|
+
)`;
|
|
9848
|
+
function ensureSyncMetaTable(db) {
|
|
9849
|
+
db.exec(SYNC_META_TABLE_SQL);
|
|
9850
|
+
}
|
|
9851
|
+
function getSyncMeta(db, table) {
|
|
9852
|
+
ensureSyncMetaTable(db);
|
|
9853
|
+
return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
|
|
9854
|
+
}
|
|
9855
|
+
function upsertSyncMeta(db, meta) {
|
|
9856
|
+
ensureSyncMetaTable(db);
|
|
9857
|
+
const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
|
|
9858
|
+
if (existing) {
|
|
9859
|
+
db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
|
|
9860
|
+
} else {
|
|
9861
|
+
db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
function transferRows(source, target, table, rows, options) {
|
|
9865
|
+
const { primaryKey = "id", conflictColumn = "updated_at" } = options;
|
|
9866
|
+
let written = 0;
|
|
9867
|
+
let skipped = 0;
|
|
9868
|
+
const errors2 = [];
|
|
9869
|
+
if (rows.length === 0)
|
|
9870
|
+
return { written, skipped, errors: errors2 };
|
|
9871
|
+
const columns = Object.keys(rows[0]);
|
|
9872
|
+
const hasConflictCol = columns.includes(conflictColumn);
|
|
9873
|
+
const hasPrimaryKey = columns.includes(primaryKey);
|
|
9874
|
+
if (!hasPrimaryKey) {
|
|
9875
|
+
errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
|
|
9876
|
+
return { written, skipped, errors: errors2 };
|
|
9877
|
+
}
|
|
9878
|
+
for (const row of rows) {
|
|
9879
|
+
try {
|
|
9880
|
+
const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
|
|
9881
|
+
if (existing) {
|
|
9882
|
+
if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
|
|
9883
|
+
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
9884
|
+
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
9885
|
+
if (existingTime >= incomingTime) {
|
|
9886
|
+
skipped++;
|
|
9887
|
+
continue;
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
9891
|
+
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
9892
|
+
values.push(row[primaryKey]);
|
|
9893
|
+
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
9894
|
+
} else {
|
|
9895
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
9896
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9897
|
+
const values = columns.map((c) => row[c]);
|
|
9898
|
+
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
9899
|
+
}
|
|
9900
|
+
written++;
|
|
9901
|
+
} catch (err) {
|
|
9902
|
+
errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
return { written, skipped, errors: errors2 };
|
|
9906
|
+
}
|
|
9907
|
+
function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
9908
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
9909
|
+
const results = [];
|
|
9910
|
+
ensureSyncMetaTable(local);
|
|
9911
|
+
for (const table of tables) {
|
|
9912
|
+
const stat = {
|
|
9913
|
+
table,
|
|
9914
|
+
total_rows: 0,
|
|
9915
|
+
synced_rows: 0,
|
|
9916
|
+
skipped_rows: 0,
|
|
9917
|
+
errors: [],
|
|
9918
|
+
first_sync: false
|
|
9919
|
+
};
|
|
9920
|
+
try {
|
|
9921
|
+
const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
9922
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
9923
|
+
const meta = getSyncMeta(local, table);
|
|
9924
|
+
let rows;
|
|
9925
|
+
if (meta?.last_synced_at) {
|
|
9926
|
+
try {
|
|
9927
|
+
rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
9928
|
+
} catch {
|
|
9929
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
9930
|
+
stat.first_sync = true;
|
|
9931
|
+
}
|
|
9932
|
+
} else {
|
|
9933
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
9934
|
+
stat.first_sync = true;
|
|
9935
|
+
}
|
|
9936
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
9937
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
9938
|
+
const result = transferRows(local, remote, table, batch, options);
|
|
9939
|
+
stat.synced_rows += result.written;
|
|
9940
|
+
stat.skipped_rows += result.skipped;
|
|
9941
|
+
stat.errors.push(...result.errors);
|
|
9942
|
+
}
|
|
9943
|
+
if (rows.length === 0) {
|
|
9944
|
+
stat.skipped_rows = stat.total_rows;
|
|
9945
|
+
}
|
|
9946
|
+
const now = new Date().toISOString();
|
|
9947
|
+
upsertSyncMeta(local, {
|
|
9948
|
+
table_name: table,
|
|
9949
|
+
last_synced_at: now,
|
|
9950
|
+
last_synced_row_count: stat.synced_rows,
|
|
9951
|
+
direction: "push"
|
|
9952
|
+
});
|
|
9953
|
+
} catch (err) {
|
|
9954
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
9955
|
+
}
|
|
9956
|
+
results.push(stat);
|
|
9957
|
+
}
|
|
9958
|
+
return results;
|
|
9959
|
+
}
|
|
9960
|
+
function incrementalSyncPull(remote, local, tables, options = {}) {
|
|
9961
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
9962
|
+
const results = [];
|
|
9963
|
+
ensureSyncMetaTable(local);
|
|
9964
|
+
for (const table of tables) {
|
|
9965
|
+
const stat = {
|
|
9966
|
+
table,
|
|
9967
|
+
total_rows: 0,
|
|
9968
|
+
synced_rows: 0,
|
|
9969
|
+
skipped_rows: 0,
|
|
9970
|
+
errors: [],
|
|
9971
|
+
first_sync: false
|
|
9972
|
+
};
|
|
9973
|
+
try {
|
|
9974
|
+
const countResult = remote.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
9975
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
9976
|
+
const meta = getSyncMeta(local, table);
|
|
9977
|
+
let rows;
|
|
9978
|
+
if (meta?.last_synced_at) {
|
|
9979
|
+
try {
|
|
9980
|
+
rows = remote.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
9981
|
+
} catch {
|
|
9982
|
+
rows = remote.all(`SELECT * FROM "${table}"`);
|
|
9983
|
+
stat.first_sync = true;
|
|
9984
|
+
}
|
|
9985
|
+
} else {
|
|
9986
|
+
rows = remote.all(`SELECT * FROM "${table}"`);
|
|
9987
|
+
stat.first_sync = true;
|
|
9988
|
+
}
|
|
9989
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
9990
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
9991
|
+
const result = transferRows(remote, local, table, batch, options);
|
|
9992
|
+
stat.synced_rows += result.written;
|
|
9993
|
+
stat.skipped_rows += result.skipped;
|
|
9994
|
+
stat.errors.push(...result.errors);
|
|
9995
|
+
}
|
|
9996
|
+
if (rows.length === 0) {
|
|
9997
|
+
stat.skipped_rows = stat.total_rows;
|
|
9998
|
+
}
|
|
9999
|
+
const now = new Date().toISOString();
|
|
10000
|
+
upsertSyncMeta(local, {
|
|
10001
|
+
table_name: table,
|
|
10002
|
+
last_synced_at: now,
|
|
10003
|
+
last_synced_row_count: stat.synced_rows,
|
|
10004
|
+
direction: "pull"
|
|
10005
|
+
});
|
|
10006
|
+
} catch (err) {
|
|
10007
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
10008
|
+
}
|
|
10009
|
+
results.push(stat);
|
|
10010
|
+
}
|
|
10011
|
+
return results;
|
|
10012
|
+
}
|
|
10013
|
+
function getSyncMetaAll(db) {
|
|
10014
|
+
ensureSyncMetaTable(db);
|
|
10015
|
+
return db.all(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name`);
|
|
10016
|
+
}
|
|
10017
|
+
function getSyncMetaForTable(db, table) {
|
|
10018
|
+
return getSyncMeta(db, table);
|
|
10019
|
+
}
|
|
10020
|
+
function resetSyncMeta(db, table) {
|
|
10021
|
+
ensureSyncMetaTable(db);
|
|
10022
|
+
db.run(`DELETE FROM _sync_meta WHERE table_name = ?`, table);
|
|
10023
|
+
}
|
|
10024
|
+
function resetAllSyncMeta(db) {
|
|
10025
|
+
ensureSyncMetaTable(db);
|
|
10026
|
+
db.run(`DELETE FROM _sync_meta`);
|
|
10027
|
+
}
|
|
10028
|
+
// src/auto-sync.ts
|
|
10029
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
10030
|
+
import { homedir as homedir3 } from "os";
|
|
10031
|
+
import { join as join3 } from "path";
|
|
10032
|
+
var AUTO_SYNC_CONFIG_PATH = join3(homedir3(), ".hasna", "cloud", "config.json");
|
|
10033
|
+
var DEFAULT_AUTO_SYNC_CONFIG = {
|
|
10034
|
+
auto_sync_on_start: true,
|
|
10035
|
+
auto_sync_on_stop: true
|
|
10036
|
+
};
|
|
10037
|
+
function getAutoSyncConfig() {
|
|
10038
|
+
try {
|
|
10039
|
+
if (!existsSync3(AUTO_SYNC_CONFIG_PATH)) {
|
|
10040
|
+
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
10041
|
+
}
|
|
10042
|
+
const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
|
|
10043
|
+
return {
|
|
10044
|
+
auto_sync_on_start: typeof raw.auto_sync_on_start === "boolean" ? raw.auto_sync_on_start : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_start,
|
|
10045
|
+
auto_sync_on_stop: typeof raw.auto_sync_on_stop === "boolean" ? raw.auto_sync_on_stop : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_stop
|
|
10046
|
+
};
|
|
10047
|
+
} catch {
|
|
10048
|
+
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
10049
|
+
}
|
|
10050
|
+
}
|
|
10051
|
+
function executeAutoSync(event, local, remote, tables) {
|
|
10052
|
+
const direction = event === "start" ? "pull" : "push";
|
|
10053
|
+
const result = {
|
|
10054
|
+
event,
|
|
10055
|
+
direction,
|
|
10056
|
+
success: false,
|
|
10057
|
+
tables_synced: 0,
|
|
10058
|
+
total_rows_synced: 0,
|
|
10059
|
+
errors: []
|
|
10060
|
+
};
|
|
10061
|
+
try {
|
|
10062
|
+
const stats = direction === "pull" ? incrementalSyncPull(remote, local, tables) : incrementalSyncPush(local, remote, tables);
|
|
10063
|
+
for (const s of stats) {
|
|
10064
|
+
if (s.errors.length === 0) {
|
|
10065
|
+
result.tables_synced++;
|
|
10066
|
+
}
|
|
10067
|
+
result.total_rows_synced += s.synced_rows;
|
|
10068
|
+
result.errors.push(...s.errors);
|
|
10069
|
+
}
|
|
10070
|
+
result.success = result.errors.length === 0;
|
|
10071
|
+
} catch (err) {
|
|
10072
|
+
result.errors.push(err?.message ?? String(err));
|
|
10073
|
+
}
|
|
10074
|
+
return result;
|
|
10075
|
+
}
|
|
10076
|
+
var cleanupHandlers = [];
|
|
10077
|
+
var signalHandlersInstalled = false;
|
|
10078
|
+
function installSignalHandlers() {
|
|
10079
|
+
if (signalHandlersInstalled)
|
|
10080
|
+
return;
|
|
10081
|
+
signalHandlersInstalled = true;
|
|
10082
|
+
const handleExit = () => {
|
|
10083
|
+
for (const fn of cleanupHandlers) {
|
|
10084
|
+
try {
|
|
10085
|
+
fn();
|
|
10086
|
+
} catch {}
|
|
10087
|
+
}
|
|
10088
|
+
};
|
|
10089
|
+
process.on("SIGTERM", () => {
|
|
10090
|
+
handleExit();
|
|
10091
|
+
process.exit(0);
|
|
10092
|
+
});
|
|
10093
|
+
process.on("SIGINT", () => {
|
|
10094
|
+
handleExit();
|
|
10095
|
+
process.exit(0);
|
|
10096
|
+
});
|
|
10097
|
+
process.on("beforeExit", () => {
|
|
10098
|
+
handleExit();
|
|
10099
|
+
});
|
|
10100
|
+
}
|
|
10101
|
+
function setupAutoSync(serviceName, server, local, remote, tables) {
|
|
10102
|
+
const config = getAutoSyncConfig();
|
|
10103
|
+
const cloudConfig = getCloudConfig();
|
|
10104
|
+
const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
|
|
10105
|
+
const syncOnStart = () => {
|
|
10106
|
+
if (!config.auto_sync_on_start || !isSyncEnabled)
|
|
10107
|
+
return null;
|
|
10108
|
+
return executeAutoSync("start", local, remote, tables);
|
|
10109
|
+
};
|
|
10110
|
+
const syncOnStop = () => {
|
|
10111
|
+
if (!config.auto_sync_on_stop || !isSyncEnabled)
|
|
10112
|
+
return null;
|
|
10113
|
+
return executeAutoSync("stop", local, remote, tables);
|
|
10114
|
+
};
|
|
10115
|
+
if (server && typeof server.onconnect === "function") {
|
|
10116
|
+
const origOnConnect = server.onconnect;
|
|
10117
|
+
server.onconnect = (...args) => {
|
|
10118
|
+
syncOnStart();
|
|
10119
|
+
return origOnConnect.apply(server, args);
|
|
10120
|
+
};
|
|
10121
|
+
} else if (server && typeof server.on === "function") {
|
|
10122
|
+
server.on("connect", () => {
|
|
10123
|
+
syncOnStart();
|
|
10124
|
+
});
|
|
10125
|
+
}
|
|
10126
|
+
if (server && typeof server.ondisconnect === "function") {
|
|
10127
|
+
const origOnDisconnect = server.ondisconnect;
|
|
10128
|
+
server.ondisconnect = (...args) => {
|
|
10129
|
+
syncOnStop();
|
|
10130
|
+
return origOnDisconnect.apply(server, args);
|
|
10131
|
+
};
|
|
10132
|
+
} else if (server && typeof server.on === "function") {
|
|
10133
|
+
server.on("disconnect", () => {
|
|
10134
|
+
syncOnStop();
|
|
10135
|
+
});
|
|
10136
|
+
}
|
|
10137
|
+
installSignalHandlers();
|
|
10138
|
+
cleanupHandlers.push(() => {
|
|
10139
|
+
syncOnStop();
|
|
10140
|
+
});
|
|
10141
|
+
return { syncOnStart, syncOnStop, config };
|
|
10142
|
+
}
|
|
10143
|
+
function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
|
|
10144
|
+
setupAutoSync(serviceName, mcpServer, local, remote, tables);
|
|
10145
|
+
}
|
|
9504
10146
|
// src/mcp-helpers.ts
|
|
9505
10147
|
function registerCloudTools(server, serviceName) {
|
|
9506
10148
|
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
@@ -9512,10 +10154,10 @@ function registerCloudTools(server, serviceName) {
|
|
|
9512
10154
|
];
|
|
9513
10155
|
if (config.rds.host && config.rds.username) {
|
|
9514
10156
|
try {
|
|
9515
|
-
const pg2 = new
|
|
9516
|
-
pg2.get("SELECT 1 as ok");
|
|
10157
|
+
const pg2 = new PgAdapterAsync(getConnectionString("postgres"));
|
|
10158
|
+
await pg2.get("SELECT 1 as ok");
|
|
9517
10159
|
lines.push("PostgreSQL: connected");
|
|
9518
|
-
pg2.close();
|
|
10160
|
+
await pg2.close();
|
|
9519
10161
|
} catch (err) {
|
|
9520
10162
|
lines.push(`PostgreSQL: failed — ${err?.message}`);
|
|
9521
10163
|
}
|
|
@@ -9536,11 +10178,11 @@ function registerCloudTools(server, serviceName) {
|
|
|
9536
10178
|
};
|
|
9537
10179
|
}
|
|
9538
10180
|
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
9539
|
-
const cloud = new
|
|
10181
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
9540
10182
|
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
9541
|
-
const results = syncPush(local, cloud, { tables: tableList });
|
|
10183
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
9542
10184
|
local.close();
|
|
9543
|
-
cloud.close();
|
|
10185
|
+
await cloud.close();
|
|
9544
10186
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
9545
10187
|
return {
|
|
9546
10188
|
content: [{ type: "text", text: `Pushed ${total} rows across ${tableList.length} table(s).` }]
|
|
@@ -9559,17 +10201,16 @@ function registerCloudTools(server, serviceName) {
|
|
|
9559
10201
|
};
|
|
9560
10202
|
}
|
|
9561
10203
|
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
9562
|
-
const cloud = new
|
|
10204
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
9563
10205
|
let tableList;
|
|
9564
10206
|
if (tablesStr) {
|
|
9565
10207
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
9566
10208
|
} else {
|
|
9567
10209
|
try {
|
|
9568
|
-
|
|
9569
|
-
tableList = rows.map((r) => r.tablename);
|
|
10210
|
+
tableList = await listPgTables(cloud);
|
|
9570
10211
|
} catch {
|
|
9571
10212
|
local.close();
|
|
9572
|
-
cloud.close();
|
|
10213
|
+
await cloud.close();
|
|
9573
10214
|
return {
|
|
9574
10215
|
content: [
|
|
9575
10216
|
{ type: "text", text: "Error: failed to list cloud tables." }
|
|
@@ -9578,9 +10219,9 @@ function registerCloudTools(server, serviceName) {
|
|
|
9578
10219
|
};
|
|
9579
10220
|
}
|
|
9580
10221
|
}
|
|
9581
|
-
const results = syncPull(
|
|
10222
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
9582
10223
|
local.close();
|
|
9583
|
-
cloud.close();
|
|
10224
|
+
await cloud.close();
|
|
9584
10225
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
9585
10226
|
return {
|
|
9586
10227
|
content: [{ type: "text", text: `Pulled ${total} rows across ${tableList.length} table(s).` }]
|
|
@@ -9614,25 +10255,25 @@ function registerCloudCommands(program, serviceName) {
|
|
|
9614
10255
|
if (config.rds.host && config.rds.username) {
|
|
9615
10256
|
try {
|
|
9616
10257
|
const connStr = getConnectionString("postgres");
|
|
9617
|
-
const pg2 = new
|
|
9618
|
-
pg2.get("SELECT 1 as ok");
|
|
10258
|
+
const pg2 = new PgAdapterAsync(connStr);
|
|
10259
|
+
await pg2.get("SELECT 1 as ok");
|
|
9619
10260
|
console.log("PostgreSQL: connected");
|
|
9620
|
-
pg2.close();
|
|
10261
|
+
await pg2.close();
|
|
9621
10262
|
} catch (err) {
|
|
9622
10263
|
console.log("PostgreSQL: connection failed —", err?.message);
|
|
9623
10264
|
}
|
|
9624
10265
|
}
|
|
9625
10266
|
});
|
|
9626
|
-
cloudCmd.command("push").description("Push local data to cloud").option("--tables <tables>", "Comma-separated table names").action((opts) => {
|
|
10267
|
+
cloudCmd.command("push").description("Push local data to cloud").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
|
|
9627
10268
|
const config = getCloudConfig();
|
|
9628
10269
|
if (config.mode === "local") {
|
|
9629
10270
|
console.error("Error: mode is 'local'. Run `cloud setup` first.");
|
|
9630
10271
|
process.exit(1);
|
|
9631
10272
|
}
|
|
9632
10273
|
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
9633
|
-
const cloud = new
|
|
10274
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
9634
10275
|
const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
9635
|
-
const results = syncPush(local, cloud, {
|
|
10276
|
+
const results = await syncPush(local, cloud, {
|
|
9636
10277
|
tables,
|
|
9637
10278
|
onProgress: (p) => {
|
|
9638
10279
|
if (p.phase === "done") {
|
|
@@ -9641,26 +10282,25 @@ function registerCloudCommands(program, serviceName) {
|
|
|
9641
10282
|
}
|
|
9642
10283
|
});
|
|
9643
10284
|
local.close();
|
|
9644
|
-
cloud.close();
|
|
10285
|
+
await cloud.close();
|
|
9645
10286
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
9646
10287
|
console.log(`Done. ${total} rows pushed.`);
|
|
9647
10288
|
});
|
|
9648
|
-
cloudCmd.command("pull").description("Pull cloud data to local").option("--tables <tables>", "Comma-separated table names").action((opts) => {
|
|
10289
|
+
cloudCmd.command("pull").description("Pull cloud data to local").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
|
|
9649
10290
|
const config = getCloudConfig();
|
|
9650
10291
|
if (config.mode === "local") {
|
|
9651
10292
|
console.error("Error: mode is 'local'. Run `cloud setup` first.");
|
|
9652
10293
|
process.exit(1);
|
|
9653
10294
|
}
|
|
9654
10295
|
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
9655
|
-
const cloud = new
|
|
10296
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
9656
10297
|
let tables;
|
|
9657
10298
|
if (opts.tables) {
|
|
9658
10299
|
tables = opts.tables.split(",").map((t) => t.trim());
|
|
9659
10300
|
} else {
|
|
9660
|
-
|
|
9661
|
-
tables = rows.map((r) => r.tablename);
|
|
10301
|
+
tables = await listPgTables(cloud);
|
|
9662
10302
|
}
|
|
9663
|
-
const results = syncPull(
|
|
10303
|
+
const results = await syncPull(cloud, local, {
|
|
9664
10304
|
tables,
|
|
9665
10305
|
onProgress: (p) => {
|
|
9666
10306
|
if (p.phase === "done") {
|
|
@@ -9669,7 +10309,7 @@ function registerCloudCommands(program, serviceName) {
|
|
|
9669
10309
|
}
|
|
9670
10310
|
});
|
|
9671
10311
|
local.close();
|
|
9672
|
-
cloud.close();
|
|
10312
|
+
await cloud.close();
|
|
9673
10313
|
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
9674
10314
|
console.log(`Done. ${total} rows pulled.`);
|
|
9675
10315
|
});
|
|
@@ -9690,25 +10330,45 @@ export {
|
|
|
9690
10330
|
translateDdl,
|
|
9691
10331
|
syncPush,
|
|
9692
10332
|
syncPull,
|
|
10333
|
+
storeConflicts,
|
|
10334
|
+
setupAutoSync,
|
|
9693
10335
|
sendFeedback,
|
|
9694
10336
|
saveFeedback,
|
|
9695
10337
|
saveCloudConfig,
|
|
10338
|
+
resolveConflicts,
|
|
10339
|
+
resolveConflict,
|
|
10340
|
+
resetSyncMeta,
|
|
10341
|
+
resetAllSyncMeta,
|
|
9696
10342
|
registerCloudTools,
|
|
9697
10343
|
registerCloudCommands,
|
|
10344
|
+
purgeResolvedConflicts,
|
|
9698
10345
|
migrateDotfile,
|
|
9699
10346
|
listSqliteTables,
|
|
9700
10347
|
listPgTables,
|
|
9701
10348
|
listFeedback,
|
|
10349
|
+
listConflicts,
|
|
10350
|
+
incrementalSyncPush,
|
|
10351
|
+
incrementalSyncPull,
|
|
9702
10352
|
hasLegacyDotfile,
|
|
10353
|
+
getWinningData,
|
|
10354
|
+
getSyncMetaForTable,
|
|
10355
|
+
getSyncMetaAll,
|
|
9703
10356
|
getHasnaDir,
|
|
9704
10357
|
getDbPath,
|
|
9705
10358
|
getDataDir,
|
|
9706
10359
|
getConnectionString,
|
|
10360
|
+
getConflict,
|
|
9707
10361
|
getConfigPath,
|
|
9708
10362
|
getConfigDir,
|
|
9709
10363
|
getCloudConfig,
|
|
10364
|
+
getAutoSyncConfig,
|
|
10365
|
+
ensureSyncMetaTable,
|
|
9710
10366
|
ensureFeedbackTable,
|
|
10367
|
+
ensureConflictsTable,
|
|
10368
|
+
enableAutoSync,
|
|
10369
|
+
detectConflicts,
|
|
9711
10370
|
createDatabase,
|
|
10371
|
+
SyncProgressTracker,
|
|
9712
10372
|
SqliteAdapter,
|
|
9713
10373
|
PgAdapterAsync,
|
|
9714
10374
|
PgAdapter,
|