mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
55bba8e918
Differential Revision: https://phabricator.services.mozilla.com/D171055
268 lines
8.2 KiB
JavaScript
268 lines
8.2 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
Async: "resource://services-common/async.sys.mjs",
|
|
});
|
|
|
|
export class CollectionProblemData {
|
|
constructor() {
|
|
this.missingIDs = 0;
|
|
this.clientDuplicates = [];
|
|
this.duplicates = [];
|
|
this.clientMissing = [];
|
|
this.serverMissing = [];
|
|
this.serverDeleted = [];
|
|
this.serverUnexpected = [];
|
|
this.differences = [];
|
|
}
|
|
|
|
/**
|
|
* Produce a list summarizing problems found. Each entry contains {name, count},
|
|
* where name is the field name for the problem, and count is the number of times
|
|
* the problem was encountered.
|
|
*
|
|
* Validation has failed if all counts are not 0.
|
|
*/
|
|
getSummary() {
|
|
return [
|
|
{ name: "clientMissing", count: this.clientMissing.length },
|
|
{ name: "serverMissing", count: this.serverMissing.length },
|
|
{ name: "serverDeleted", count: this.serverDeleted.length },
|
|
{ name: "serverUnexpected", count: this.serverUnexpected.length },
|
|
{ name: "differences", count: this.differences.length },
|
|
{ name: "missingIDs", count: this.missingIDs },
|
|
{ name: "clientDuplicates", count: this.clientDuplicates.length },
|
|
{ name: "duplicates", count: this.duplicates.length },
|
|
];
|
|
}
|
|
}
|
|
|
|
export class CollectionValidator {
|
|
// Construct a generic collection validator. This is intended to be called by
|
|
// subclasses.
|
|
// - name: Name of the engine
|
|
// - idProp: Property that identifies a record. That is, if a client and server
|
|
// record have the same value for the idProp property, they should be
|
|
// compared against eachother.
|
|
// - props: Array of properties that should be compared
|
|
constructor(name, idProp, props) {
|
|
this.name = name;
|
|
this.props = props;
|
|
this.idProp = idProp;
|
|
|
|
// This property deals with the fact that form history records are never
|
|
// deleted from the server. The FormValidator subclass needs to ignore the
|
|
// client missing records, and it uses this property to achieve it -
|
|
// (Bug 1354016).
|
|
this.ignoresMissingClients = false;
|
|
}
|
|
|
|
// Should a custom ProblemData type be needed, return it here.
|
|
emptyProblemData() {
|
|
return new CollectionProblemData();
|
|
}
|
|
|
|
async getServerItems(engine) {
|
|
let collection = engine.itemSource();
|
|
let collectionKey = engine.service.collectionKeys.keyForCollection(
|
|
engine.name
|
|
);
|
|
collection.full = true;
|
|
let result = await collection.getBatched();
|
|
if (!result.response.success) {
|
|
throw result.response;
|
|
}
|
|
let cleartexts = [];
|
|
|
|
await lazy.Async.yieldingForEach(result.records, async record => {
|
|
await record.decrypt(collectionKey);
|
|
cleartexts.push(record.cleartext);
|
|
});
|
|
|
|
return cleartexts;
|
|
}
|
|
|
|
// Should return a promise that resolves to an array of client items.
|
|
getClientItems() {
|
|
return Promise.reject("Must implement");
|
|
}
|
|
|
|
/**
|
|
* Can we guarantee validation will fail with a reason that isn't actually a
|
|
* problem? For example, if we know there are pending changes left over from
|
|
* the last sync, this should resolve to false. By default resolves to true.
|
|
*/
|
|
async canValidate() {
|
|
return true;
|
|
}
|
|
|
|
// Turn the client item into something that can be compared with the server item,
|
|
// and is also safe to mutate.
|
|
normalizeClientItem(item) {
|
|
return Cu.cloneInto(item, {});
|
|
}
|
|
|
|
// Turn the server item into something that can be easily compared with the client
|
|
// items.
|
|
async normalizeServerItem(item) {
|
|
return item;
|
|
}
|
|
|
|
// Return whether or not a server item should be present on the client. Expected
|
|
// to be overridden.
|
|
clientUnderstands(item) {
|
|
return true;
|
|
}
|
|
|
|
// Return whether or not a client item should be present on the server. Expected
|
|
// to be overridden
|
|
async syncedByClient(item) {
|
|
return true;
|
|
}
|
|
|
|
// Compare the server item and the client item, and return a list of property
|
|
// names that are different. Can be overridden if needed.
|
|
getDifferences(client, server) {
|
|
let differences = [];
|
|
for (let prop of this.props) {
|
|
let clientProp = client[prop];
|
|
let serverProp = server[prop];
|
|
if ((clientProp || "") !== (serverProp || "")) {
|
|
differences.push(prop);
|
|
}
|
|
}
|
|
return differences;
|
|
}
|
|
|
|
// Returns an object containing
|
|
// problemData: an instance of the class returned by emptyProblemData(),
|
|
// clientRecords: Normalized client records
|
|
// records: Normalized server records,
|
|
// deletedRecords: Array of ids that were marked as deleted by the server.
|
|
async compareClientWithServer(clientItems, serverItems) {
|
|
const yieldState = lazy.Async.yieldState();
|
|
|
|
const clientRecords = [];
|
|
|
|
await lazy.Async.yieldingForEach(
|
|
clientItems,
|
|
item => {
|
|
clientRecords.push(this.normalizeClientItem(item));
|
|
},
|
|
yieldState
|
|
);
|
|
|
|
const serverRecords = [];
|
|
await lazy.Async.yieldingForEach(
|
|
serverItems,
|
|
async item => {
|
|
serverRecords.push(await this.normalizeServerItem(item));
|
|
},
|
|
yieldState
|
|
);
|
|
|
|
let problems = this.emptyProblemData();
|
|
let seenServer = new Map();
|
|
let serverDeleted = new Set();
|
|
let allRecords = new Map();
|
|
|
|
for (let record of serverRecords) {
|
|
let id = record[this.idProp];
|
|
if (!id) {
|
|
++problems.missingIDs;
|
|
continue;
|
|
}
|
|
if (record.deleted) {
|
|
serverDeleted.add(record);
|
|
} else {
|
|
let serverHasPossibleDupe = seenServer.has(id);
|
|
if (serverHasPossibleDupe) {
|
|
problems.duplicates.push(id);
|
|
} else {
|
|
seenServer.set(id, record);
|
|
allRecords.set(id, { server: record, client: null });
|
|
}
|
|
record.understood = this.clientUnderstands(record);
|
|
}
|
|
}
|
|
|
|
let seenClient = new Map();
|
|
for (let record of clientRecords) {
|
|
let id = record[this.idProp];
|
|
record.shouldSync = await this.syncedByClient(record);
|
|
let clientHasPossibleDupe = seenClient.has(id);
|
|
if (clientHasPossibleDupe && record.shouldSync) {
|
|
// Only report duplicate client IDs for syncable records.
|
|
problems.clientDuplicates.push(id);
|
|
continue;
|
|
}
|
|
seenClient.set(id, record);
|
|
let combined = allRecords.get(id);
|
|
if (combined) {
|
|
combined.client = record;
|
|
} else {
|
|
allRecords.set(id, { client: record, server: null });
|
|
}
|
|
}
|
|
|
|
for (let [id, { server, client }] of allRecords) {
|
|
if (!client && !server) {
|
|
throw new Error("Impossible: no client or server record for " + id);
|
|
} else if (server && !client) {
|
|
if (!this.ignoresMissingClients && server.understood) {
|
|
problems.clientMissing.push(id);
|
|
}
|
|
} else if (client && !server) {
|
|
if (client.shouldSync) {
|
|
problems.serverMissing.push(id);
|
|
}
|
|
} else {
|
|
if (!client.shouldSync) {
|
|
if (!problems.serverUnexpected.includes(id)) {
|
|
problems.serverUnexpected.push(id);
|
|
}
|
|
continue;
|
|
}
|
|
let differences = this.getDifferences(client, server);
|
|
if (differences && differences.length) {
|
|
problems.differences.push({ id, differences });
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
problemData: problems,
|
|
clientRecords,
|
|
records: serverRecords,
|
|
deletedRecords: [...serverDeleted],
|
|
};
|
|
}
|
|
|
|
async validate(engine) {
|
|
let start = Cu.now();
|
|
let clientItems = await this.getClientItems();
|
|
let serverItems = await this.getServerItems(engine);
|
|
let serverRecordCount = serverItems.length;
|
|
let result = await this.compareClientWithServer(clientItems, serverItems);
|
|
let end = Cu.now();
|
|
let duration = end - start;
|
|
engine._log.debug(`Validated ${this.name} in ${duration}ms`);
|
|
engine._log.debug(`Problem summary`);
|
|
for (let { name, count } of result.problemData.getSummary()) {
|
|
engine._log.debug(` ${name}: ${count}`);
|
|
}
|
|
return {
|
|
duration,
|
|
version: this.version,
|
|
problems: result.problemData,
|
|
recordCount: serverRecordCount,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Default to 0, some engines may override.
|
|
CollectionValidator.prototype.version = 0;
|