/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @author Sebastian Șandru <sebastian@refinio.net>
* @author Maximilian Kallert <max@refinio.net>
* @copyright REFINIO GmbH 2021
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* The main module for CRDTs:
* * Read and write CRDT objects
* * Generate CRDT metadata
* * merge CRDTs
* * how to resolve references
* @module
*/
import type {OneCrdtToMetaObjectInterfaces} from '@OneObjectInterfaces';
import {
adjustCrdtConfig,
getCrdtMetaRecipeName,
isCrdtMetaObjectForCrdtObjectTypeName,
isCrdtMetaObjectTypeNameForCrdtObjectTypeName,
isCrdtTypeName
} from './crdt-recipes';
import {getCrdtImplementation} from './crdt/CRDTRegistry';
import {createError} from './errors';
import {parseHeader} from './microdata-to-object';
import {getRecipe, isVersionedObjectType, resolveRuleInheritance} from './object-recipes';
import type {
CRDTMetaData,
MetaObjectList,
OneCrdtMetaObjectTypes,
OneCrdtObjectTypeNames,
OneCrdtObjectTypes,
OneObjectTypeNames,
OneObjectTypes,
OneUnversionedObjectTypes,
Recipe,
RecipeRule
} from './recipes';
import {CREATION_STATUS} from './storage-base-common';
import {getObject, getObjectWithType, storeUnversionedObject} from './storage-unversioned-objects';
import type {VersionedObjectResult} from './storage-versioned-objects';
import {
getObjectByIdHash,
getObjectByIdObj,
storeVersionedObject,
storeVersionedObjectOmitVersionMap,
versionedObjEvent
} from './storage-versioned-objects';
import {readUTF8TextFile} from './system/storage-base';
import {iterateArrayFromEnd} from './util/function';
import {calculateIdHashOfObj, MICRODATA_START} from './util/object';
import {serializeWithType} from './util/promise';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';
import {ensureHash, isListItemType, ruleHasItemType} from './util/type-checks';
import {isObject} from './util/type-checks-basic';
import {createVersionMapOrAppendEntry, VERSION_UPDATES} from './version-map-updater';
const DUMMY_RULE_ITEMPROP = 'DUMMY';
/**
* This stores a new version for a crdt based data type.
*
* Functionality:
* 1) A diff is calculated between ob and objBase.
* 2) The metadata is updated based on the changes. The details depend on the CRDT
* implementation. For a set a list of add / remove objects might be generated, or for a
* LWWRegister the current time might be recorded for the new value ...
* 3) This metadata is then merged into the latest version generating a new version for the
* versioned object.
*
* The interface differs from the normal storeVersionedObject only by one additional
* parameter: objBase. For most CRDTs we need to calculate the changes that
* have happened since the last version. So you have to specify on which version
* you based your new object on.
* We can't just use the latest version that is stored, because other changes might have arrived
* via chum in the meantime.
*
* @param {T} obj
* @param {SHA256Hash<T>} objBase
* @returns {Promise<VersionedObjectResult<T>>}
*/
export async function storeVersionedObjectCRDT<T extends OneCrdtObjectTypes>(
obj: T,
objBase?: SHA256Hash<T>
): Promise<VersionedObjectResult<T>> {
// Calculate the new metadata by
// 1) grabbing the old metadata (from objBase)
// 2) update the metadata by using the diff between obj and objBase
// TODO: Perhaps it is enough not return the whole metadata, but just the
// operations that are required to do the diff.
// The next merge step then will integrate it.
const metaData = await generateCrdtMetaData(obj, objBase);
const {hash} = await storeVersionedObjectOmitVersionMap(obj);
return mergeCRDTData(
obj,
{
$type$: getCrdtMetaRecipeName<T['$type$']>(obj.$type$),
data: hash,
crdt: metaData
} as CRDTMetaData<T>,
'local'
);
}
/**
* Generate a new version of the versioned object by merging the passed information.
* The data is referenced by the metadata object, so effectively you pass in metadata and data.
* @param {CRDTMetaData<T.$type$>} metadata
* @returns {Promise<VersionedObjectResult<T>>}
*/
export async function storeVersionedObjectCRDTMergeOnly<T extends OneCrdtObjectTypes>(
metadata: CRDTMetaData<T>
): Promise<VersionedObjectResult<T>> {
const obj = await getObject(metadata.data);
return mergeCRDTData(obj, metadata, 'remote');
}
// ######## CRDT METADATA STEP ########
/**
* This generates / updates metadata for all crdt variables.
*
* This function diffs the obj against the base object in order to see what data was added /
* removed from the object. Then it computes new metadata that then can be merged into the
* versioned object with the mergeCRDTData call.
*
* Note: This call does not write anything at the moment. It just gets the base version,
* compares it to the new version and returns the metadata that reflects this diff.
*
* @param {T} obj - The new version of the object that shall be written
* @param {SHA256Hash<T>} objBaseHash - The base object. It is used to see what changes were
* made in the original object.
* @returns {Promise<unknown>} - This
*/
async function generateCrdtMetaData<T extends OneCrdtObjectTypes>(
obj: T,
objBaseHash?: SHA256Hash<T>
): Promise<Record<string, any>> {
// Step 1: Get the crdt, metadata object and the recipe
const objBaseData = objBaseHash
? await getCrdtAndMetaObject(objBaseHash)
: {
data: undefined,
metadata: {
crdt: undefined
}
};
const recipe = getRecipe(obj.$type$);
// Step 2: Iterate the object / recipe and then call the crdt implementations
const objectCRDT = getCrdtImplementation('object');
const metadata = await objectCRDT.generateMetaData(
objBaseData.data,
obj,
objBaseData.metadata.crdt,
{
itemprop: DUMMY_RULE_ITEMPROP,
itemtype: {type: 'object', rules: recipe.rule}
},
recipe.crdtConfig
);
if (!isObject(metadata)) {
throw new Error(`Object CRDT generated metadata is not an object: ${metadata}`);
}
return metadata;
}
/**
* Generate the metadata for the given rule. If case, it is recursive for unversioned reference
* objects.
* @param {RecipeRule} rule
* @param {Record<string, any>} obj
* @param {Record<string, any>} [objBase]
* @param {Record<string, any>} [metadataBase]
* @param {Recipe.crdtConfig} [crdtConfig]
* @returns {Promise<undefined|{itemprop:string, metaDataPart:*}>}
*/
export async function generateCrdtMetadataForRule(
rule: RecipeRule,
obj: Record<string, any>,
objBase?: Record<string, any>,
metadataBase?: Record<string, any>,
crdtConfig?: Recipe['crdtConfig']
): Promise<undefined | ReturnType<typeof generateMetadataForListRule>> {
const actualRule = resolveRuleInheritance(rule);
const adjustedCrdtConfig = adjustCrdtConfig(rule, crdtConfig);
const objChild = obj && obj[actualRule.itemprop];
const objBaseChild = objBase && objBase[actualRule.itemprop];
const metadataBaseChild = metadataBase && metadataBase[actualRule.itemprop];
// If the objects don't have data, then we need to skip this rule.
// If we don't stop we might have an endless loop, because rules can be recursive.
if (objChild === undefined && objBaseChild === undefined) {
return;
}
if (ruleHasItemType(actualRule)) {
// Handle lists by choosing the default list crdt
if (isListItemType(actualRule.itemtype)) {
return await generateMetadataForListRule(
actualRule,
objBaseChild,
objChild,
metadataBaseChild,
adjustedCrdtConfig
);
} else if (actualRule.itemtype.type === 'object') {
// Handle objects
const objectCRDT = getCrdtImplementation('object');
const metaDataPart = await objectCRDT.generateMetaData(
objBaseChild,
objChild,
metadataBaseChild,
actualRule,
adjustedCrdtConfig
);
return {itemprop: actualRule.itemprop, metaDataPart};
} else if (
actualRule.itemtype.type === 'referenceToObj' &&
'allowedTypes' in actualRule.itemtype &&
actualRule.itemtype.allowedTypes !== undefined
) {
// Handle links to objects
return await generateMetadataForRuleWithReferenceToObj(
actualRule,
objBaseChild,
objChild,
metadataBaseChild,
adjustedCrdtConfig
);
}
}
// Handle normal types, id links CLOB and BLOB types as register
const registerCrdt = getCrdtImplementation(
'register',
crdtConfig ? crdtConfig.get(rule.itemprop) : undefined
);
const metaDataPart = await registerCrdt.generateMetaData(
objBaseChild,
objChild,
metadataBaseChild,
actualRule
);
return {itemprop: actualRule.itemprop, metaDataPart};
}
// ######## CRDT MERGE STEP ########
/**
* Merges an object with CRDT Annotations into a versioned object
*
* This will generate a new version of the versioned object (if necessary).
*
* @param {T} obj
* @param {CRDTMetaData<T.$type$>} metadata
* @param {"local" | "remote"} source
* @returns {Promise<VersionedObjectResult<T>>}
*/
async function mergeCRDTData<T extends OneCrdtObjectTypes>(
obj: T,
metadata: CRDTMetaData<T>,
source?: 'local' | 'remote'
): Promise<VersionedObjectResult<T>> {
const versionIdHash = await calculateIdHashOfObj(obj);
return serializeWithType(`crdtmerge_${versionIdHash}`, async () => {
let latestVersion;
try {
latestVersion = await getCrdtAndMetaObjectByIdHash(versionIdHash);
} catch (ignore) {
latestVersion = {
data: undefined,
metadata: {
$type$: metadata.$type$,
crdt: undefined,
data: undefined
}
};
}
const recipe = getRecipe(obj.$type$);
const objectCRDT = getCrdtImplementation('object');
if (objectCRDT === undefined) {
throw new Error('Object CRDT implementation not found.');
}
const mergedData = await objectCRDT.mergeMetaData(
latestVersion.data,
latestVersion.metadata.crdt,
obj,
metadata.crdt,
{
itemprop: DUMMY_RULE_ITEMPROP,
itemtype: {type: 'object', rules: recipe.rule}
},
recipe.crdtConfig
);
if (!isObject(mergedData) || !isObject(mergedData.data) || !isObject(mergedData.metadata)) {
throw new Error(`Object CRDT merged metadata result is invalid: ${mergedData}`);
}
(mergedData.data as T).$type$ = obj.$type$;
const objResult = await storeVersionedObjectOmitVersionMap(mergedData.data as T);
return storeCrdtAndMetaObject({
$type$: getCrdtMetaRecipeName<T['$type$']>(obj.$type$),
data: objResult.hash,
crdt: mergedData.metadata,
source: source
});
});
}
/**
* Merge CRDT data for given rule. If case, it is recursive for unversioned referenced objects.
* @param {Record<string, any> | undefined} objLatestVersion
* @param {Record<string, any> | undefined} metadataLatestVersion
* @param {Record<string, any> | undefined} objToMerge
* @param {Record<string, any> | undefined} metadataToMerge
* @param {RecipeRule} rule
* @param {Recipe.crdtConfig} crdtConfig
* @returns {Promise<undefined|{itemprop:string, data:*, metadata:*}>}
*/
export async function mergeCRDTDataForRule(
objLatestVersion: Record<string, any> | undefined,
metadataLatestVersion: Record<string, any> | undefined,
objToMerge: Record<string, any> | undefined,
metadataToMerge: Record<string, any> | undefined,
rule: RecipeRule,
crdtConfig?: Recipe['crdtConfig']
): Promise<undefined | ReturnType<typeof mergeCRDTDataForListRule>> {
const actualRule = resolveRuleInheritance(rule);
const adjustedCrdtConfig = adjustCrdtConfig(rule, crdtConfig);
const objLatestVersionChild = objLatestVersion && objLatestVersion[actualRule.itemprop];
const metadataLatestVersionChild =
metadataLatestVersion && metadataLatestVersion[actualRule.itemprop];
const objToMergeChild = objToMerge && objToMerge[actualRule.itemprop];
const metadataToMergeChild = metadataToMerge && metadataToMerge[actualRule.itemprop];
// If the objects don't have data, then we need to skip this rule.
// If we don't stop we might have an endless loop, because rules can be recursive.
if (objLatestVersionChild === undefined && objToMergeChild === undefined) {
return;
}
// Handle lists by choosing the default list crdt
if (ruleHasItemType(rule) && isListItemType(rule.itemtype)) {
return await mergeCRDTDataForListRule(
actualRule,
objLatestVersionChild,
metadataLatestVersionChild,
objToMergeChild,
metadataToMergeChild,
adjustedCrdtConfig
);
} else if (ruleHasItemType(rule) && rule.itemtype.type === 'object') {
// Handle child rules by handling members individually
const objectCRDT = getCrdtImplementation('object');
const mergeData = await objectCRDT.mergeMetaData(
objLatestVersionChild,
metadataLatestVersionChild,
objToMergeChild,
metadataToMergeChild,
actualRule,
adjustedCrdtConfig
);
return {
itemprop: actualRule.itemprop,
data: mergeData.data,
metadata: mergeData.metadata
};
// eslint-disable-next-line no-negated-condition
} else if (
ruleHasItemType(actualRule) &&
'allowedTypes' in actualRule.itemtype &&
actualRule.itemtype.allowedTypes !== undefined &&
actualRule.itemtype.type === 'referenceToObj'
) {
// Handle links to objects
return await mergeCRDTDataForRuleWithReferenceToObj(
actualRule,
objLatestVersionChild,
metadataLatestVersionChild,
metadataToMergeChild,
objToMergeChild,
adjustedCrdtConfig
);
}
// Handle normal types, id links CLOB and BLOB types as register
const registerCrdt = getCrdtImplementation(
'register',
adjustedCrdtConfig ? adjustedCrdtConfig.get(rule.itemprop) : undefined
);
const mergeData = await registerCrdt.mergeMetaData(
objLatestVersionChild,
metadataLatestVersionChild,
objToMergeChild,
metadataToMergeChild,
actualRule
);
return {
itemprop: actualRule.itemprop,
data: mergeData.data,
metadata: mergeData.metadata
};
}
// ######## Low level get / store of object and metadata object ########
/**
* Loads the latest data and metadata object for the versioned object.
*
* @param {SHA256IdHash<T>} idHash
* @returns {{data: T, metadata: CRDTMetaData<T>}}
*/
async function getCrdtAndMetaObjectByIdHash<T extends OneCrdtObjectTypes>(
idHash: SHA256IdHash<T>
): Promise<{data: T; metadata: OneCrdtToMetaObjectInterfaces[T['$type$']]}> {
const latestObject = await getObjectByIdHash(idHash);
return {
data: latestObject.obj,
metadata: await getCrdtMetaObjectWithType(latestObject.hash, latestObject.obj.$type$)
};
}
/**
* Loads the latest data and metadata object for the versioned object.
* @template T
* @param {SHA256Hash<T>} hash - The hash of a specific object version for which to load data
* and metadata.
* @returns {{data: T, metadata: unknown}}
*/
async function getCrdtAndMetaObject<T extends OneCrdtObjectTypes>(
hash: SHA256Hash<T>
): Promise<{data: T; metadata: OneCrdtToMetaObjectInterfaces[T['$type$']]}> {
const requestedObject = await getObject(hash);
return {
data: requestedObject,
metadata: await getCrdtMetaObjectWithType(hash, requestedObject.$type$)
};
}
/**
* Obtain the meta-object of a certain type for the passed object hash.
* @param {SHA256Hash<T>} hash
* @param {OneCrdtObjectTypeNames} type
* @returns {Promise<OneUnversionedObjectTypes>}
*/
export async function getCrdtMetaObjectWithType<T extends OneCrdtObjectTypes>(
hash: SHA256Hash<T>,
type: T['$type$']
): Promise<OneCrdtToMetaObjectInterfaces[T['$type$']]> {
const metaObjectList = await getObjectByIdObj({
$type$: 'MetaObjectList',
object: hash
});
for (const metaObject of iterateArrayFromEnd(metaObjectList.obj.metaobject)) {
// We don't have to load all objects. In 99% of the time the last object is the correct one.
// For the 1% we need to loop.
// eslint-disable-next-line no-await-in-loop
const metaObj = await getObject(metaObject);
// I am not sure wy the explicit generic argument is required. I guess this has
// something to do with getObjectByIdObj not resolving T of latestObject properly.
if (isCrdtMetaObjectForCrdtObjectTypeName(metaObj, type)) {
return metaObj;
}
}
throw new Error(`No metadata exists for the specified version. The data structure is
corrupt!. Hash: ${hash} Type: ${type}`);
}
/**
* Obtain the meta-object for the passed object hash.
* @param {SHA256Hash<T>} hash
* @param {OneCrdtObjectTypeNames} type
* @returns {Promise<OneCrdtMetaObjectTypes>}
*/
export async function getCrdtMetaObject(
hash: SHA256Hash<OneCrdtObjectTypes>,
type: OneCrdtObjectTypeNames
): Promise<OneCrdtMetaObjectTypes> {
const metaObjectList = await getObjectByIdObj({
$type$: 'MetaObjectList',
object: hash
});
for (const metaObject of iterateArrayFromEnd(metaObjectList.obj.metaobject)) {
// We don't have to load all objects. In 99% of the time the last object is the correct one.
// For the 1% we need to loop.
// eslint-disable-next-line no-await-in-loop
const metaObj = await getObject(metaObject);
// I am not sure wy the explicit generic argument is required. I guess this has
// something to do with getObjectByIdObj not resolving T of latestObject properly.
if (isCrdtMetaObjectForCrdtObjectTypeName(metaObj, type)) {
return metaObj;
}
}
throw new Error(
`No metadata exists for the specified version. The data structure is corrupt!
Hash: ${hash} Type: ${type}`
);
}
/**
* Obtain the meta-object for the passed object hash.
* @param {SHA256Hash<T>} hash
* @param {OneCrdtObjectTypeNames} type
* @returns {Promise<{microdata:string, hash:SHA256Hash<OneCrdtMetaObjectTypes>}>}
*/
export async function getCrdtMetaObjectAsMicrodata(
hash: SHA256Hash<OneCrdtObjectTypes>,
type: OneCrdtObjectTypeNames
): Promise<{microdata: string; hash: SHA256Hash<OneCrdtMetaObjectTypes>}> {
const metaObjectList = await getObjectByIdObj({
$type$: 'MetaObjectList',
object: hash
});
for (const metaobject of iterateArrayFromEnd(metaObjectList.obj.metaobject)) {
// Read the file to get the type
// It is okay to do this in a loop, because in 99% of time the first entry
// will be the desired file.
// eslint-disable-next-line no-await-in-loop
const microdata = await readUTF8TextFile(metaobject);
if (!microdata.startsWith(MICRODATA_START)) {
throw createError('CRDT-GCMOAM001', {hash});
}
// Get the tye from file
const metaType = parseHeader(undefined, {
html: microdata,
isIdObj: false,
position: 0
});
if (!isCrdtTypeName(type)) {
throw createError('CRDT-GCMOAM002', {hash});
}
// I am not sure wy the explicit generic argument is required. I guess this has
// something to do with getObjectByIdObj not resolving T of latestObject properly.
if (isCrdtMetaObjectTypeNameForCrdtObjectTypeName(metaType, type)) {
return {
microdata: microdata,
hash: metaobject as SHA256Hash<OneCrdtMetaObjectTypes>
};
}
}
throw new Error(
`No metadata exists for the specified version. The data structure is corrupt!
Hash: ${hash} Type: ${type}`
);
}
/**
* Stores a new version of the data and metadata.
*
* The data is not part of the interface because we expect the data object already be written to
* disk - but without being recorded in the version map. This can be achieved with the call
* storeVersionedObjectOmitVersionMap.
*
* @param {CRDTMetaData<T.$type$>} metadata
* @returns {Promise<VersionedObjectResult<T>>}
*/
async function storeCrdtAndMetaObject<T extends OneCrdtObjectTypes>(
metadata: CRDTMetaData<T>
): Promise<VersionedObjectResult<T>> {
// TODO: check this 'as' conversion again.
const metadataResult = await storeUnversionedObject(
metadata as unknown as OneCrdtToMetaObjectInterfaces[T['$type$']]
);
// Generate the metadata object list that links the metadata
let newMetaObjectList: MetaObjectList;
try {
newMetaObjectList = (
await getObjectByIdObj({
$type$: 'MetaObjectList',
object: metadata.data
})
).obj;
newMetaObjectList.metaobject.push(metadataResult.hash);
} catch (ignore) {
newMetaObjectList = {
$type$: 'MetaObjectList',
object: metadata.data,
metaobject: [metadataResult.hash]
};
}
// Create a new version for the metadata and after that for the data.
// The order is important, because if the code is interrupted, and you
// wrote the metadata after the data itself, then we might have a version
// without metadata which will corrupt the CRDTs.
await storeVersionedObject(newMetaObjectList);
// Create a new version for the data.
// The object already exists, though it is not part of the version map!
// TODO: There are two ways to make it more efficient
// 1) Pass the object as argument to this function (bad: caller might make an error)
// 2) Implement a storeVersionedObject function that can accept a hash instead of an
// object.
const obj = await getObject(metadata.data);
const objId = await calculateIdHashOfObj(obj);
const vmapResult = await createVersionMapOrAppendEntry(
objId,
metadata.data,
CREATION_STATUS.NEW,
VERSION_UPDATES.NONE_IF_LATEST
);
const objCreationResult = {
obj,
status: vmapResult.status,
timestamp: vmapResult.timestamp,
hash: metadata.data,
idHash: objId as any // TODO THIS TS ERROR IS SO USELESS!
};
versionedObjEvent.dispatch(objCreationResult);
return objCreationResult;
}
/**
* Generate the metadata object for the given list rule.
* @param {RecipeRule} rule
* @param {unknown} objBaseChild
* @param {unknown} objChild
* @param {unknown} metadataBaseChild
* @param {Recipe.crdtConfig} crdtConfig
* @returns {{itemprop: string, metaDataPart: unknown}}
*/
async function generateMetadataForListRule(
rule: RecipeRule,
objBaseChild: unknown,
objChild: unknown,
metadataBaseChild: unknown,
crdtConfig?: Recipe['crdtConfig']
): Promise<{itemprop: string; metaDataPart: unknown}> {
const listCrdt = getCrdtImplementation(
'list',
crdtConfig ? crdtConfig.get(rule.itemprop) : undefined
);
const metaDataPart = await listCrdt.generateMetaData(
objBaseChild,
objChild,
metadataBaseChild,
rule
);
return {itemprop: rule.itemprop, metaDataPart};
}
/**
* Generate the metadata object for the given rule with a 'referenceToObject' property. The
* metadata generation will go recursively if:
* - the referenced object is unversioned
* - the referenced object has only one single type
* - the referenced object has a concrete type ('*' - not accepted; it is handled with CRDT-Register
* algorithm)
* @param {RecipeRule} rule
* @param {unknown} objBaseChild
* @param {unknown} objChild
* @param {unknown} metadataBaseChild
* @param {Recipe.crdtConfig} crdtConfig
* @returns {Promise<{itemprop: string, metaDataPart: unknown}>}
*/
async function generateMetadataForRuleWithReferenceToObj(
rule: RecipeRule,
objBaseChild: unknown,
objChild: unknown,
metadataBaseChild: unknown,
crdtConfig: Recipe['crdtConfig']
): Promise<{itemprop: string; metaDataPart: unknown}> {
// by default, rule.itemtype is string
if (rule.itemtype === undefined) {
rule.itemtype = {type: 'string'};
}
if (rule.itemtype.type !== 'referenceToObj') {
throw new Error(`referenceToObj is undefined for rule: ${rule}`);
}
const objChildHash = ensureHash<OneObjectTypes>(objChild);
const objBaseChildHash =
objBaseChild === undefined ? undefined : ensureHash<OneObjectTypes>(objBaseChild);
if (metadataBaseChild !== undefined && !isObject(metadataBaseChild)) {
throw new Error(
`generateMetadataForRuleWithReferenceToObj metadataBaseChild not object: ${metadataBaseChild}`
);
}
const referenceToObjType = rule.itemtype.allowedTypes.values().next().value;
if (
!isVersionedObjectType(referenceToObjType) &&
referenceToObjType !== '*' &&
rule.itemtype.allowedTypes.size === 1 &&
(rule.optional === false || rule.optional === undefined)
) {
const recipeOfUnversionedObj = getRecipe(referenceToObjType as OneObjectTypeNames);
const objectChild = await getObjectWithType(objChildHash);
let objectBaseChild = undefined;
if (objBaseChildHash !== undefined) {
objectBaseChild = await getObjectWithType(objBaseChildHash);
}
const objectCRDT = getCrdtImplementation('object');
const metaDataPart = await objectCRDT.generateMetaData(
objectBaseChild,
objectChild,
metadataBaseChild,
{
itemprop: DUMMY_RULE_ITEMPROP,
itemtype: {type: 'object', rules: recipeOfUnversionedObj.rule}
},
crdtConfig
);
return {itemprop: rule.itemprop, metaDataPart};
}
const registerCrdt = getCrdtImplementation(
'register',
crdtConfig ? crdtConfig.get(rule.itemprop) : undefined
);
const metaDataPart = await registerCrdt.generateMetaData(
objBaseChildHash,
objChildHash,
metadataBaseChild,
rule
);
return {itemprop: rule.itemprop, metaDataPart};
}
/**
* Merge CRDT data for list rule.
* @param {RecipeRule} rule
* @param {unknown} objLatestVersionChildHash
* @param {unknown} metadataLatestVersionChild
* @param {unknown} objToMergeChild
* @param {unknown} metadataToMergeChild
* @param {Recipe.crdtConfig} crdtConfig
* @returns {Promise<{itemprop:string, data:*, metadata:*}>}
*/
async function mergeCRDTDataForListRule(
rule: RecipeRule,
objLatestVersionChildHash: unknown,
metadataLatestVersionChild: unknown,
objToMergeChild: unknown,
metadataToMergeChild: unknown,
crdtConfig?: Recipe['crdtConfig']
): Promise<{itemprop: string; data: unknown; metadata: unknown}> {
const listCrdt = getCrdtImplementation(
'list',
crdtConfig ? crdtConfig.get(rule.itemprop) : undefined
);
const mergeData = await listCrdt.mergeMetaData(
objLatestVersionChildHash,
metadataLatestVersionChild,
objToMergeChild,
metadataToMergeChild,
rule
);
return {
itemprop: rule.itemprop,
data: mergeData.data,
metadata: mergeData.metadata
};
}
/**
* Merge CRDT data for the given rule with a 'referenceToObject' property. The data is merging
* is done recursively if:
* - the referenced object is unversioned
* - the referenced object has only one single type
* - the referenced object has a concrete type ('*' - not accepted; it is handled with CRDT-Register
* algorithm)
* @param {RecipeRule} rule
* @param {unknown} objLatestVersionChild
* @param {unknown} metadataLatestVersionChild
* @param {unknown} metadataToMergeChild
* @param {unknown} objToMergeChild
* @param {Recipe.crdtConfig} crdtConfig
* @returns {Promise<{itemprop: string, data: unknown, metadata: unknown}>}
*/
async function mergeCRDTDataForRuleWithReferenceToObj(
rule: RecipeRule,
objLatestVersionChild: unknown,
metadataLatestVersionChild: unknown,
metadataToMergeChild: unknown,
objToMergeChild: unknown,
crdtConfig: Recipe['crdtConfig']
): Promise<{itemprop: string; data: unknown; metadata: unknown}> {
if (rule.itemtype === undefined) {
rule.itemtype = {type: 'string'};
}
if (rule.itemtype.type !== 'referenceToObj') {
throw new Error(`referenceToObj is undefined for rule: ${rule}`);
}
const objToMergeChildHash = ensureHash<OneObjectTypes>(objToMergeChild);
const objLatestVersionChildHash =
objLatestVersionChild === undefined
? undefined
: ensureHash<OneObjectTypes>(objLatestVersionChild);
if (metadataLatestVersionChild !== undefined && !isObject(metadataLatestVersionChild)) {
throw new Error(
`metadataLatestVersionChild is defined, but it is not an object :${metadataLatestVersionChild}`
);
}
if (!isObject(metadataToMergeChild)) {
throw new Error(`metadataToMergeChild is not an object: ${metadataToMergeChild}`);
}
const referenceToObjType = rule.itemtype.allowedTypes.values().next().value;
if (
!isVersionedObjectType(referenceToObjType) &&
referenceToObjType !== '*' &&
rule.itemtype.allowedTypes.size === 1 &&
(rule.optional === false || rule.optional === undefined)
) {
// Handle child rules by handling members individually
const recipeOfUnversionedObj = getRecipe(referenceToObjType as OneObjectTypeNames);
const objectToMergeChild = await getObjectWithType(objToMergeChildHash);
let objectLatestVersionChild = undefined;
if (objLatestVersionChildHash !== undefined) {
objectLatestVersionChild = await getObjectWithType(objLatestVersionChildHash);
}
const objectCRDT = getCrdtImplementation('object');
const mergeData = await objectCRDT.mergeMetaData(
objectLatestVersionChild,
metadataLatestVersionChild,
objectToMergeChild,
metadataToMergeChild,
{
itemprop: DUMMY_RULE_ITEMPROP,
itemtype: {type: 'object', rules: recipeOfUnversionedObj.rule}
},
crdtConfig
);
if (!isObject(mergeData) || !isObject(mergeData.data) || !isObject(mergeData.metadata)) {
throw new Error(`Object CRDT merged metadata result is invalid: ${mergeData}`);
}
mergeData.data.$type$ = objectToMergeChild.$type$;
const unversionedObjectResult = await storeUnversionedObject(
mergeData.data as OneUnversionedObjectTypes
);
return {
itemprop: rule.itemprop,
data: unversionedObjectResult.hash,
metadata: mergeData.metadata
};
}
// Handle referenced object non-recursively
const registerCrdt = getCrdtImplementation(
'register',
crdtConfig ? crdtConfig.get(rule.itemprop) : undefined
);
const mergeData = await registerCrdt.mergeMetaData(
objLatestVersionChildHash,
metadataLatestVersionChild,
objToMergeChildHash,
metadataToMergeChild,
rule
);
return {
itemprop: rule.itemprop,
data: mergeData.data,
metadata: mergeData.metadata
};
}