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