UNPKG

rxdb

Version:

A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/

392 lines (388 loc) 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RxDBcrdtPlugin = exports.RX_CRDT_CONTEXT = void 0; exports.getCRDTConflictHandler = getCRDTConflictHandler; exports.getCRDTSchemaPart = getCRDTSchemaPart; exports.hashCRDTOperations = hashCRDTOperations; exports.insertCRDT = insertCRDT; exports.mergeCRDTFields = mergeCRDTFields; exports.rebuildFromCRDT = rebuildFromCRDT; exports.sortOperationComparator = sortOperationComparator; exports.updateCRDT = updateCRDT; var _rxError = require("../../rx-error.js"); var _index = require("../../plugins/utils/index.js"); var _index2 = require("../../index.js"); var _mingoUpdater = require("../update/mingo-updater.js"); async function updateCRDT(entry) { entry = _index2.overwritable.deepFreezeWhenDevMode(entry); var jsonSchema = this.collection.schema.jsonSchema; if (!jsonSchema.crdt) { throw (0, _rxError.newRxError)('CRDT1', { schema: jsonSchema, queryObj: entry }); } var crdtOptions = (0, _index.ensureNotFalsy)(jsonSchema.crdt); var storageToken = await this.collection.database.storageToken; return this.incrementalModify(async docData => { var crdtDocField = (0, _index.clone)((0, _index.getProperty)(docData, crdtOptions.field)); var operation = { body: (0, _index.toArray)(entry), creator: storageToken, time: (0, _index.now)() }; /** * A new write will ALWAYS be an operation in the last * array which was non existing before. */ var lastAr = [operation]; crdtDocField.operations.push(lastAr); crdtDocField.hash = await hashCRDTOperations(this.collection.database.hashFunction, crdtDocField); docData = runOperationOnDocument(this.collection.schema.jsonSchema, docData, operation); (0, _index.setProperty)(docData, crdtOptions.field, crdtDocField); return docData; }, RX_CRDT_CONTEXT); } async function insertCRDT(entry) { entry = _index2.overwritable.deepFreezeWhenDevMode(entry); var jsonSchema = this.schema.jsonSchema; if (!jsonSchema.crdt) { throw (0, _rxError.newRxError)('CRDT1', { schema: jsonSchema, queryObj: entry }); } var crdtOptions = (0, _index.ensureNotFalsy)(jsonSchema.crdt); var storageToken = await this.database.storageToken; var operation = { body: Array.isArray(entry) ? entry : [entry], creator: storageToken, time: (0, _index.now)() }; var insertData = {}; insertData = runOperationOnDocument(this.schema.jsonSchema, insertData, operation); var crdtDocField = { operations: [], hash: '' }; (0, _index.setProperty)(insertData, crdtOptions.field, crdtDocField); var lastAr = [operation]; crdtDocField.operations.push(lastAr); crdtDocField.hash = await hashCRDTOperations(this.database.hashFunction, crdtDocField); var result = await this.insert(insertData).catch(async err => { if (err.code === 'CONFLICT') { // was a conflict, update document instead of inserting var doc = await this.findOne(err.parameters.id).exec(true); return doc.updateCRDT(entry); } else { throw err; } }); return result; } function sortOperationComparator(a, b) { return a.creator > b.creator ? 1 : -1; } function runOperationOnDocument(schema, docData, operation) { var entryParts = operation.body; entryParts.forEach(entryPart => { var isMatching; if (entryPart.selector) { var query = { selector: (0, _index.ensureNotFalsy)(entryPart.selector), sort: [], skip: 0 }; var matcher = (0, _index2.getQueryMatcher)(schema, query); isMatching = matcher(docData); } else { isMatching = true; } if (isMatching) { if (entryPart.ifMatch) { docData = (0, _mingoUpdater.mingoUpdater)(docData, entryPart.ifMatch); } } else { if (entryPart.ifNotMatch) { docData = (0, _mingoUpdater.mingoUpdater)(docData, entryPart.ifNotMatch); } } }); return docData; } async function hashCRDTOperations(hashFunction, crdts) { var hashObj = crdts.operations.map(operations => { return operations.map(op => op.creator); }); var hash = await hashFunction(JSON.stringify(hashObj)); return hash; } function getCRDTSchemaPart() { var operationSchema = { type: 'object', properties: { body: { type: 'array', items: { type: 'object', properties: { selector: { type: 'object' }, ifMatch: { type: 'object' }, ifNotMatch: { type: 'object' } }, additionalProperties: false }, minItems: 1 }, creator: { type: 'string' }, time: { type: 'number', minimum: 1, maximum: 1000000000000000, multipleOf: 0.01 } }, additionalProperties: false, required: ['body', 'creator', 'time'] }; return { type: 'object', properties: { operations: { type: 'array', items: { type: 'array', items: operationSchema } }, hash: { type: 'string', // set a minLength to not accidentally store an empty string minLength: 2 } }, additionalProperties: false, required: ['operations', 'hash'] }; } async function mergeCRDTFields(hashFunction, crdtsA, crdtsB) { // the value with most operations must be A to // ensure we not miss out rows when iterating over both fields. if (crdtsA.operations.length < crdtsB.operations.length) { [crdtsA, crdtsB] = [crdtsB, crdtsA]; } var ret = { operations: [], hash: '' }; crdtsA.operations.forEach((row, index) => { var mergedOps = []; var ids = new Set(); // used to deduplicate row.forEach(op => { ids.add(op.creator); mergedOps.push(op); }); if (crdtsB.operations[index]) { crdtsB.operations[index].forEach(op => { if (!ids.has(op.creator)) { mergedOps.push(op); } }); } mergedOps = mergedOps.sort(sortOperationComparator); ret.operations[index] = mergedOps; }); ret.hash = await hashCRDTOperations(hashFunction, ret); return ret; } function rebuildFromCRDT(schema, docData, crdts) { var base = { _deleted: false }; (0, _index.setProperty)(base, (0, _index.ensureNotFalsy)(schema.crdt).field, crdts); crdts.operations.forEach(operations => { operations.forEach(op => { base = runOperationOnDocument(schema, base, op); }); }); return base; } function getCRDTConflictHandler(hashFunction, schema) { var crdtOptions = (0, _index.ensureNotFalsy)(schema.crdt); var crdtField = crdtOptions.field; var getCRDTValue = (0, _index.objectPathMonad)(crdtField); var conflictHandler = { isEqual(a, b, ctx) { return getCRDTValue(a).hash === getCRDTValue(b).hash; }, async resolve(i) { var newDocCrdt = getCRDTValue(i.newDocumentState); var masterDocCrdt = getCRDTValue(i.realMasterState); var mergedCrdt = await mergeCRDTFields(hashFunction, newDocCrdt, masterDocCrdt); var mergedDoc = rebuildFromCRDT(schema, i.newDocumentState, mergedCrdt); return mergedDoc; } }; return conflictHandler; } var RX_CRDT_CONTEXT = exports.RX_CRDT_CONTEXT = 'rx-crdt'; var RxDBcrdtPlugin = exports.RxDBcrdtPlugin = { name: 'crdt', rxdb: true, prototypes: { RxDocument: proto => { proto.updateCRDT = updateCRDT; var oldRemove = proto.remove; proto.remove = function () { if (!this.collection.schema.jsonSchema.crdt) { return oldRemove.bind(this)(); } return this.updateCRDT({ ifMatch: { $set: { _deleted: true } } }); }; var oldincrementalPatch = proto.incrementalPatch; proto.incrementalPatch = function (patch) { if (!this.collection.schema.jsonSchema.crdt) { return oldincrementalPatch.bind(this)(patch); } return this.updateCRDT({ ifMatch: { $set: patch } }); }; var oldincrementalModify = proto.incrementalModify; proto.incrementalModify = function (fn, context) { if (!this.collection.schema.jsonSchema.crdt) { return oldincrementalModify.bind(this)(fn); } if (context === RX_CRDT_CONTEXT) { return oldincrementalModify.bind(this)(fn); } else { throw (0, _rxError.newRxError)('CRDT2', { id: this.primary, args: { context } }); } }; }, RxCollection: proto => { proto.insertCRDT = insertCRDT; } }, overwritable: {}, hooks: { preCreateRxCollection: { after: data => { if (!data.schema.crdt) { return; } if (data.conflictHandler) { throw (0, _rxError.newRxError)('CRDT3', { collection: data.name, schema: data.schema }); } data.conflictHandler = getCRDTConflictHandler(data.database.hashFunction, data.schema); } }, createRxCollection: { after: ({ collection }) => { if (!collection.schema.jsonSchema.crdt) { return; } var crdtOptions = (0, _index.ensureNotFalsy)(collection.schema.jsonSchema.crdt); var crdtField = crdtOptions.field; var getCrdt = (0, _index.objectPathMonad)(crdtOptions.field); /** * In dev-mode we have to ensure that all document writes * have the correct crdt state so that nothing is missed out * or could accidentally do non-crdt writes to the document. */ if (_index2.overwritable.isDevMode()) { var bulkWriteBefore = collection.storageInstance.bulkWrite.bind(collection.storageInstance); collection.storageInstance.bulkWrite = async function (writes, context) { await Promise.all(writes.map(async write => { var newDocState = (0, _index.clone)(write.document); var crdts = getCrdt(newDocState); var rebuild = rebuildFromCRDT(collection.schema.jsonSchema, newDocState, crdts); function docWithoutMeta(doc) { var ret = {}; Object.entries(doc).forEach(([k, v]) => { if (!k.startsWith('_') && typeof v !== 'undefined') { ret[k] = v; } }); return ret; } if (!(0, _index.deepEqual)(docWithoutMeta(newDocState), docWithoutMeta(rebuild))) { throw (0, _rxError.newRxError)('SNH', { document: newDocState }); } var recalculatedHash = await hashCRDTOperations(collection.database.hashFunction, crdts); if (crdts.hash !== recalculatedHash) { throw (0, _rxError.newRxError)('SNH', { document: newDocState, args: { hash: crdts.hash, recalculatedHash } }); } })); return bulkWriteBefore(writes, context); }; } var bulkInsertBefore = collection.bulkInsert.bind(collection); collection.bulkInsert = async function (docsData) { var storageToken = await collection.database.storageToken; var useDocsData = await Promise.all(docsData.map(async docData => { var setMe = {}; Object.entries(docData).forEach(([key, value]) => { if (!key.startsWith('_') && key !== crdtField) { setMe[key] = value; } }); var crdtOperations = { operations: [[{ creator: storageToken, body: [{ ifMatch: { $set: setMe } }], time: (0, _index.now)() }]], hash: '' }; crdtOperations.hash = await hashCRDTOperations(collection.database.hashFunction, crdtOperations); (0, _index.setProperty)(docData, crdtOptions.field, crdtOperations); return docData; })); return bulkInsertBefore(useDocsData); }; } } } }; //# sourceMappingURL=index.js.map