/**
* @author Maximilian Kallert <max@refinio.net>
* @author Eduard Reimer <eduard@refinio.net>
* @copyright REFINIO GmbH 2020
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 1.0.0
*/
/**
* This module implements the object CRDT, which in itself is no CRDT algorithm but
* the toplevel object which resolves nested objects.
* @module
*/
import {generateCrdtMetadataForRule, mergeCRDTDataForRule} from '../crdt';
import {generateMetaRecipeForRule} from '../crdt-recipes';
import {resolveRuleInheritance} from '../object-recipes';
import {ruleHasItemType} from '../util/type-checks';
import {isObject} from '../util/type-checks-basic';
import type {CRDTImplementation} from './CRDTImplementation';
import type {Recipe, RecipeRule} from '../recipes';
/**
* Implementation of Object CRDT. This is a parent for the other CRDT implementations.
*
* See paper:
* A comprehensive study of Convergent and Commutative Replicated Data Types
* Marc Shapiro, Nuno PreguiƧa, Carlos Baquero, Marek Zawirski
* p. 26, Section 3.3.5
* https://hal.inria.fr/inria-00555588/document
*/
export class ObjectCRDT implements CRDTImplementation {
generateRecipeRules(
rule: RecipeRule,
path: string,
crdtConfig?: Recipe['crdtConfig']
): RecipeRule[] {
const crdtRules: RecipeRule[] = [];
if (!(ruleHasItemType(rule) && rule.itemtype.type === 'object')) {
throw new Error(
`Object parameter in type.object CRDT implementation is not an object ${JSON.stringify(
rule
)}`
);
}
for (const actualRule of rule.itemtype.rules) {
const resolvedRule = resolveRuleInheritance(actualRule);
const subrule: RecipeRule = {
itemprop: actualRule.itemprop,
itemtype: {
type: 'object',
rules: generateMetaRecipeForRule(resolvedRule, path, crdtConfig)
}
};
crdtRules.push(subrule);
}
return crdtRules;
}
async generateMetaData(
dataOld: unknown,
dataNew: unknown,
metadataOld: unknown,
rule: RecipeRule,
crdtConfig?: Recipe['crdtConfig']
): Promise<Record<string, any>> {
const metaData: Record<string, any> = {};
if (dataOld !== undefined && !isObject(dataOld)) {
throw new Error(`Old data is neither undefined or an object: ${dataOld}`);
}
if (!isObject(dataNew)) {
throw new Error(`New data is not an object: ${dataNew}`);
}
if (metadataOld !== undefined && !isObject(metadataOld)) {
throw new Error(`Old metadata is neither undefined or an object: ${metadataOld}`);
}
if (!(ruleHasItemType(rule) && rule.itemtype.type === 'object')) {
throw new Error(
`Object parameter in type.object CRDT implementation is not an object ${JSON.stringify(
rule
)}`
);
}
const metaDataParts = await Promise.all(
rule.itemtype.rules.map(async actualRule => {
return await generateCrdtMetadataForRule(
actualRule,
dataNew,
dataOld,
metadataOld,
crdtConfig
);
})
);
// Build to object after all the promises are settled to ensure the same order of the props
for (const metaDataPart of metaDataParts) {
if (metaDataPart === undefined) {
continue;
}
metaData[metaDataPart.itemprop] = metaDataPart.metaDataPart;
}
return metaData;
}
async mergeMetaData(
objLatestVersion: unknown,
metadataLatestVersion: unknown,
objToMerge: unknown,
metadataToMerge: unknown,
rule: Readonly<RecipeRule>,
crdtConfig?: Recipe['crdtConfig']
): Promise<{metadata: Record<string, any>; data: Record<string, any>}> {
const objMerged: Record<string, any> = {};
const metadataMerged: Record<string, any> = {};
if (objLatestVersion !== undefined && !isObject(objLatestVersion)) {
throw new Error(`Old data is neither undefined or an object: ${objLatestVersion}`);
}
if (metadataLatestVersion !== undefined && !isObject(metadataLatestVersion)) {
throw new Error(
`Old metadata is neither undefined or an object: ${metadataLatestVersion}`
);
}
if (!isObject(objToMerge)) {
throw new Error(`The object to merge is not an object: ${objToMerge}`);
}
if (!isObject(metadataToMerge)) {
throw new Error(`The metadata to merge is not an object: ${metadataToMerge}`);
}
if (!(ruleHasItemType(rule) && rule.itemtype.type === 'object')) {
throw new Error(
`Object parameter in type.object CRDT implementation is not an object ${JSON.stringify(
rule
)}`
);
}
const mergeObjects = await Promise.all(
rule.itemtype.rules.map(async actualRule => {
return await mergeCRDTDataForRule(
objLatestVersion,
metadataLatestVersion,
objToMerge,
metadataToMerge,
actualRule,
crdtConfig
);
})
);
// Build to object after all the promises are settled to ensure the same order of the props
for (const mergeObject of mergeObjects) {
if (mergeObject === undefined) {
continue;
}
objMerged[mergeObject.itemprop] = mergeObject.data;
metadataMerged[mergeObject.itemprop] = mergeObject.metadata;
}
return {
data: objMerged,
metadata: metadataMerged
};
}
}