@nozbe/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
460 lines (453 loc) • 27.5 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.default = diagnoseSyncConsistency;
var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose"));
var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
var _forEachAsync = _interopRequireDefault(require("../../utils/fp/forEachAsync"));
var _sync = require("../../sync");
var _RawRecord = require("../../RawRecord");
var _impl = require("../../sync/impl");
var _helpers = require("../../sync/impl/helpers");
var _censorRaw = _interopRequireDefault(require("../censorRaw"));
var _excluded = ["_status", "_changed"];
/* eslint-disable no-continue */
var yieldLog = function () {
return new Promise(function (resolve) {
setTimeout(resolve, 0);
});
};
var recordsToMap = function (records) {
var map = new Map();
records.forEach(function (record) {
if (map.has(record.id)) {
throw new Error("\u274C Array of records has a duplicate ID ".concat(record.id));
}
map.set(record.id, record);
});
return map;
};
var renderRecord = function (record) {
// eslint-disable-next-line no-unused-vars
var rest = (0, _objectWithoutPropertiesLoose2.default)(record, _excluded);
return JSON.stringify((0, _censorRaw.default)(rest), null, ' ');
};
// Indicates uncertainty whether local and remote states are fully synced - requires a retry
var InconsistentSyncError = /*#__PURE__*/function (_Error) {
function InconsistentSyncError() {
return _Error.apply(this, arguments) || this;
}
(0, _inheritsLoose2.default)(InconsistentSyncError, _Error);
return InconsistentSyncError;
}(/*#__PURE__*/(0, _wrapNativeSuper2.default)(Error));
function diagnoseSyncConsistencyImpl({
db: db,
synchronize: synchronize,
pullChanges: pullChanges,
isInconsistentRecordAllowed = function () {
return new Promise(function ($return) {
return $return(false);
});
},
isExcessLocalRecordAllowed = function () {
return new Promise(function ($return) {
return $return(false);
});
},
isMissingLocalRecordAllowed = function () {
return new Promise(function ($return) {
return $return(false);
});
}
}, log) {
return new Promise(function ($return, $error) {
var totalCorruptionCount;
log('# Sync consistency diagnostics');
log();
totalCorruptionCount = 0;
// synchronize first, to ensure we're at consistent state
// (twice to deal with just-resolved conflicts or data just pushed)
log('Syncing once...');
return Promise.resolve(synchronize()).then(function () {
try {
log('Syncing twice...');
return Promise.resolve(synchronize()).then(function () {
try {
log('Synced.');
// disallow further local changes
return Promise.resolve(db.read(function (reader) {
return new Promise(function ($return, $error) {
var schema, allUserData, lastPulledAt, recentChanges, recentChangeCount, collections;
return Promise.resolve(reader.callReader(function () {
return (0, _sync.hasUnsyncedChanges)({
database: db
});
})).then(function ($await_9) {
try {
// ensure no more local changes
if ($await_9) {
log('❌ Sync consistency diagnostics failed because there are unsynced local changes - please try again.');
return $error(new InconsistentSyncError('unsynced local changes'));
}
log();
// fetch ALL data
log('Fetching all data. This may take a while (same as initial login), please be patient...');
({
schema: schema
} = db);
return Promise.resolve(pullChanges({
lastPulledAt: null,
schemaVersion: schema.version,
migration: null
})).then(function ($await_10) {
try {
allUserData = $await_10;
log("Fetched all ".concat((0, _helpers.changeSetCount)(allUserData), " records"));
// Ensure that all data is consistent with current data - if so,
// an incremental sync will be empty
// NOTE: Fetching all data takes enough time that there's a great risk
// that many test will fail here. It would be easier to fetch all data
// first and then do a quick incremental sync, but that doesn't give us
// a guarantee of consistency
log("Ensuring no new remote changes...");
return Promise.resolve((0, _impl.getLastPulledAt)(db)).then(function ($await_11) {
try {
lastPulledAt = $await_11;
return Promise.resolve(pullChanges({
lastPulledAt: lastPulledAt,
schemaVersion: schema.version,
migration: null
})).then(function ($await_12) {
try {
recentChanges = $await_12;
recentChangeCount = (0, _helpers.changeSetCount)(recentChanges);
if (0 < recentChangeCount) {
log("\u274C Sync consistency diagnostics failed because there were changes on the server between initial synchronization and now. Please try again.");
log();
return $error(new InconsistentSyncError('there were changes on the server between initial synchronization and now'));
}
log();
// Compare all the data
collections = Object.keys(db.collections.map);
return Promise.resolve((0, _forEachAsync.default)(collections, function (table) {
return new Promise(function ($return, $error) {
var tableCorruptionCount, records, created, updated, deleted, remoteRecords, localMap, tableSchema, remoteMap, inconsistentRecords, excessRecords, missingRecords, columnsToCheck;
log("## Consistency of `".concat(table, "`"));
log();
return Promise.resolve(yieldLog()).then(function () {
try {
tableCorruptionCount = 0;
return Promise.resolve(db.collections
// $FlowFixMe
.get(table).query().fetch()).then(function ($await_14) {
try {
records = $await_14;
({
created: created,
updated: updated,
deleted: deleted
} = allUserData[table]);
if (deleted.length) {
log("\u2753 Warning: ".concat(deleted.length, " deleted ").concat(table, " found in full (login) sync -- should not be necessary:"));
log(deleted.join(','));
}
remoteRecords = created.concat(updated);
log("Found ".concat(records.length, " `").concat(table, "` locally, ").concat(remoteRecords.length, " remotely"));
// Transform records into hash maps for efficient lookup
localMap = recordsToMap(records.map(function (r) {
return r._raw;
}));
tableSchema = schema.tables[table];
remoteMap = recordsToMap(remoteRecords.map(function (r) {
return (0, _RawRecord.sanitizedRaw)(r, tableSchema);
}));
return Promise.resolve(yieldLog()).then(function () {
try {
inconsistentRecords = [];
excessRecords = [];
missingRecords = [];
return Promise.resolve((0, _forEachAsync.default)(Array.from(remoteMap.entries()), function ([id, remote]) {
return new Promise(function ($return, $error) {
var local = localMap.get(id);
if (!local) {
return Promise.resolve(isMissingLocalRecordAllowed({
tableName: table,
remote: remote
})).then(function ($await_16) {
try {
if ($await_16) {
missingRecords.push(id);
} else {
log();
log("\u274C MISSING: Record `".concat(table, ".").concat(id, "` is present on server, but missing in local db"));
log();
log('```');
log("REMOTE: ".concat(renderRecord(remote)));
log('```');
tableCorruptionCount += 1;
}
return function () {
return $return();
}.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}
return function () {
return $return();
}.call(this);
});
})).then(function () {
try {
return Promise.resolve(yieldLog()).then(function () {
try {
columnsToCheck = tableSchema.columnArray.map(function (column) {
return column.name;
});
return Promise.resolve((0, _forEachAsync.default)(Array.from(localMap.entries()), function ([id, record]) {
return new Promise(function ($return, $error) {
var local, remote, recordIsConsistent, inconsistentColumns;
local = record;
remote = remoteMap.get(id);
// console.log(id, local, remote)
if (!remote) {
return Promise.resolve(isExcessLocalRecordAllowed({
tableName: table,
local: local
})).then(function ($await_19) {
try {
if ($await_19) {
excessRecords.push(id);
} else {
log();
log("\u274C EXCESS: Record `".concat(table, ".").concat(id, "` is present in local db, but not on server"));
log();
log('```');
log("LOCAL: ".concat(renderRecord(local)));
log('```');
tableCorruptionCount += 1;
}
return $If_3.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
} else {
recordIsConsistent = local.id === remote.id && 'synced' === local._status && '' === local._changed && columnsToCheck.every(function (column) {
return local[column] === remote[column];
});
if (!recordIsConsistent) {
inconsistentColumns = columnsToCheck.filter(function (column) {
return local[column] !== remote[column];
});
return Promise.resolve(isInconsistentRecordAllowed({
tableName: table,
local: local,
remote: remote,
inconsistentColumns: inconsistentColumns
})).then(function ($await_20) {
try {
if ($await_20) {
inconsistentRecords.push(id);
} else {
tableCorruptionCount += 1;
log();
log("\u274C INCONSISTENCY: Record `".concat(table, ".").concat(id, "` differs between server and local db"));
log();
log('```');
log("LOCAL: ".concat(renderRecord(local)));
log("REMOTE: ".concat(renderRecord(remote)));
log("DIFFERENCE:");
inconsistentColumns.forEach(function (column) {
log("- ".concat(column, " | local: ").concat(JSON.stringify(local[column]), " | remote: ").concat(JSON.stringify(remote[column])));
});
log('```');
}
return $If_4.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}
function $If_4() {
return $If_3.call(this);
}
return $If_4.call(this);
}
function $If_3() {
return $return();
}
});
})).then(function () {
try {
log();
if (inconsistentRecords.length) {
log("\u2753 Config allowed ".concat(inconsistentRecords.length, " inconsistent `").concat(table, "`"));
// log(inconsistentRecords.join(','))
}
if (excessRecords.length) {
log("\u2753 Config allowed ".concat(excessRecords.length, " excess local `").concat(table, "`"));
// log(excessRecords.join(','))
}
if (missingRecords.length) {
log("\u2753 Config allowed ".concat(missingRecords.length, " locally missing `").concat(table, "`"));
// log(missingRecords.join(','))
}
if (!tableCorruptionCount) {
log("No corruption found in this table");
}
totalCorruptionCount += tableCorruptionCount;
log();
return Promise.resolve(yieldLog()).then(function () {
try {
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
});
})).then(function () {
try {
log('## Conclusion');
log();
if (totalCorruptionCount) {
log("\u274C ".concat(totalCorruptionCount, " issues found"));
} else {
log("\u2705 No corruption found in this database!");
}
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
});
})).then(function () {
try {
return $return(totalCorruptionCount);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
});
}
function diagnoseSyncConsistency(options) {
return new Promise(function ($return, $error) {
var startTime, logText, log, allowedAttempts, issueCount;
startTime = Date.now();
logText = '';
log = function (text = '') {
var _options$log;
logText = "".concat(logText, "\n").concat(text);
null === (_options$log = options.log) || void 0 === _options$log ? void 0 : _options$log.call(options, text);
};
allowedAttempts = 5;
var $Loop_5_trampoline;
function $Loop_5() {
if (true) {
allowedAttempts -= 1;
var $Try_1_Post = function () {
try {
return $Loop_5;
} catch ($boundEx) {
return $error($boundEx);
}
};
var $Try_1_Catch = function (error) {
try {
if (error instanceof InconsistentSyncError && 1 <= allowedAttempts) {
return $Loop_5;
} else {
throw error;
}
return $Try_1_Post();
} catch ($boundEx) {
return $error($boundEx);
}
};
try {
return Promise.resolve(diagnoseSyncConsistencyImpl(options, log)).then(function ($await_25) {
try {
issueCount = $await_25;
log();
log("Done in ".concat((Date.now() - startTime) / 1000, " s."));
return $return({
issueCount: issueCount,
log: logText
});
} catch ($boundEx) {
return $Try_1_Catch($boundEx);
}
}, $Try_1_Catch);
} catch (error) {
$Try_1_Catch(error)
}
} else return [1];
}
return ($Loop_5_trampoline = function (q) {
while (q) {
if (q.then) return void q.then($Loop_5_trampoline, $error);
try {
if (q.pop) {
if (q.length) return q.pop() ? $Loop_5_exit.call(this) : q;else q = $Loop_5;
} else q = q.call(this);
} catch (_exception) {
return $error(_exception);
}
}
}.bind(this))($Loop_5);
function $Loop_5_exit() {
// eslint-disable-next-line no-unreachable
return $error(new Error('unreachable'));
}
});
}