collection_validator.sys.mjs (8415B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 Async: "resource://services-common/async.sys.mjs", 9 }); 10 11 export class CollectionProblemData { 12 constructor() { 13 this.missingIDs = 0; 14 this.clientDuplicates = []; 15 this.duplicates = []; 16 this.clientMissing = []; 17 this.serverMissing = []; 18 this.serverDeleted = []; 19 this.serverUnexpected = []; 20 this.differences = []; 21 } 22 23 /** 24 * Produce a list summarizing problems found. Each entry contains {name, count}, 25 * where name is the field name for the problem, and count is the number of times 26 * the problem was encountered. 27 * 28 * Validation has failed if all counts are not 0. 29 */ 30 getSummary() { 31 return [ 32 { name: "clientMissing", count: this.clientMissing.length }, 33 { name: "serverMissing", count: this.serverMissing.length }, 34 { name: "serverDeleted", count: this.serverDeleted.length }, 35 { name: "serverUnexpected", count: this.serverUnexpected.length }, 36 { name: "differences", count: this.differences.length }, 37 { name: "missingIDs", count: this.missingIDs }, 38 { name: "clientDuplicates", count: this.clientDuplicates.length }, 39 { name: "duplicates", count: this.duplicates.length }, 40 ]; 41 } 42 } 43 44 export class CollectionValidator { 45 // Construct a generic collection validator. This is intended to be called by 46 // subclasses. 47 // - name: Name of the engine 48 // - idProp: Property that identifies a record. That is, if a client and server 49 // record have the same value for the idProp property, they should be 50 // compared against eachother. 51 // - props: Array of properties that should be compared 52 constructor(name, idProp, props) { 53 this.name = name; 54 this.props = props; 55 this.idProp = idProp; 56 57 // This property deals with the fact that form history records are never 58 // deleted from the server. The FormValidator subclass needs to ignore the 59 // client missing records, and it uses this property to achieve it - 60 // (Bug 1354016). 61 this.ignoresMissingClients = false; 62 } 63 64 // Should a custom ProblemData type be needed, return it here. 65 emptyProblemData() { 66 return new CollectionProblemData(); 67 } 68 69 async getServerItems(engine) { 70 let collection = engine.itemSource(); 71 let collectionKey = engine.service.collectionKeys.keyForCollection( 72 engine.name 73 ); 74 collection.full = true; 75 let result = await collection.getBatched(); 76 if (!result.response.success) { 77 throw result.response; 78 } 79 let cleartexts = []; 80 81 await lazy.Async.yieldingForEach(result.records, async record => { 82 await record.decrypt(collectionKey); 83 cleartexts.push(record.cleartext); 84 }); 85 86 return cleartexts; 87 } 88 89 // Should return a promise that resolves to an array of client items. 90 getClientItems() { 91 return Promise.reject("Must implement"); 92 } 93 94 /** 95 * Can we guarantee validation will fail with a reason that isn't actually a 96 * problem? For example, if we know there are pending changes left over from 97 * the last sync, this should resolve to false. By default resolves to true. 98 */ 99 async canValidate() { 100 return true; 101 } 102 103 // Turn the client item into something that can be compared with the server item, 104 // and is also safe to mutate. 105 normalizeClientItem(item) { 106 return Cu.cloneInto(item, {}); 107 } 108 109 // Turn the server item into something that can be easily compared with the client 110 // items. 111 async normalizeServerItem(item) { 112 return item; 113 } 114 115 // Return whether or not a server item should be present on the client. Expected 116 // to be overridden. 117 clientUnderstands() { 118 return true; 119 } 120 121 // Return whether or not a client item should be present on the server. Expected 122 // to be overridden 123 async syncedByClient() { 124 return true; 125 } 126 127 // Compare the server item and the client item, and return a list of property 128 // names that are different. Can be overridden if needed. 129 getDifferences(client, server) { 130 let differences = []; 131 for (let prop of this.props) { 132 let clientProp = client[prop]; 133 let serverProp = server[prop]; 134 if ((clientProp || "") !== (serverProp || "")) { 135 differences.push(prop); 136 } 137 } 138 return differences; 139 } 140 141 // Returns an object containing 142 // problemData: an instance of the class returned by emptyProblemData(), 143 // clientRecords: Normalized client records 144 // records: Normalized server records, 145 // deletedRecords: Array of ids that were marked as deleted by the server. 146 async compareClientWithServer(clientItems, serverItems) { 147 const yieldState = lazy.Async.yieldState(); 148 149 const clientRecords = []; 150 151 await lazy.Async.yieldingForEach( 152 clientItems, 153 item => { 154 clientRecords.push(this.normalizeClientItem(item)); 155 }, 156 yieldState 157 ); 158 159 const serverRecords = []; 160 await lazy.Async.yieldingForEach( 161 serverItems, 162 async item => { 163 serverRecords.push(await this.normalizeServerItem(item)); 164 }, 165 yieldState 166 ); 167 168 let problems = this.emptyProblemData(); 169 let seenServer = new Map(); 170 let serverDeleted = new Set(); 171 let allRecords = new Map(); 172 173 for (let record of serverRecords) { 174 let id = record[this.idProp]; 175 if (!id) { 176 ++problems.missingIDs; 177 continue; 178 } 179 if (record.deleted) { 180 serverDeleted.add(record); 181 } else { 182 let serverHasPossibleDupe = seenServer.has(id); 183 if (serverHasPossibleDupe) { 184 problems.duplicates.push(id); 185 } else { 186 seenServer.set(id, record); 187 allRecords.set(id, { server: record, client: null }); 188 } 189 record.understood = this.clientUnderstands(record); 190 } 191 } 192 193 let seenClient = new Map(); 194 for (let record of clientRecords) { 195 let id = record[this.idProp]; 196 record.shouldSync = await this.syncedByClient(record); 197 let clientHasPossibleDupe = seenClient.has(id); 198 if (clientHasPossibleDupe && record.shouldSync) { 199 // Only report duplicate client IDs for syncable records. 200 problems.clientDuplicates.push(id); 201 continue; 202 } 203 seenClient.set(id, record); 204 let combined = allRecords.get(id); 205 if (combined) { 206 combined.client = record; 207 } else { 208 allRecords.set(id, { client: record, server: null }); 209 } 210 } 211 212 for (let [id, { server, client }] of allRecords) { 213 if (!client && !server) { 214 throw new Error("Impossible: no client or server record for " + id); 215 } else if (server && !client) { 216 if (!this.ignoresMissingClients && server.understood) { 217 problems.clientMissing.push(id); 218 } 219 } else if (client && !server) { 220 if (client.shouldSync) { 221 problems.serverMissing.push(id); 222 } 223 } else { 224 if (!client.shouldSync) { 225 if (!problems.serverUnexpected.includes(id)) { 226 problems.serverUnexpected.push(id); 227 } 228 continue; 229 } 230 let differences = this.getDifferences(client, server); 231 if (differences && differences.length) { 232 problems.differences.push({ id, differences }); 233 } 234 } 235 } 236 return { 237 problemData: problems, 238 clientRecords, 239 records: serverRecords, 240 deletedRecords: [...serverDeleted], 241 }; 242 } 243 244 async validate(engine) { 245 let start = ChromeUtils.now(); 246 let clientItems = await this.getClientItems(); 247 let serverItems = await this.getServerItems(engine); 248 let serverRecordCount = serverItems.length; 249 let result = await this.compareClientWithServer(clientItems, serverItems); 250 let end = ChromeUtils.now(); 251 let duration = end - start; 252 engine._log.debug(`Validated ${this.name} in ${duration}ms`); 253 engine._log.debug(`Problem summary`); 254 for (let { name, count } of result.problemData.getSummary()) { 255 engine._log.debug(` ${name}: ${count}`); 256 } 257 return { 258 duration, 259 version: this.version, 260 problems: result.problemData, 261 recordCount: serverRecordCount, 262 }; 263 } 264 } 265 266 // Default to 0, some engines may override. 267 CollectionValidator.prototype.version = 0;