/**
* @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 CRDT LWWRegister algorithm.
* @module
*/
import {convertValue} from '../object-to-microdata';
import type {RecipeRule} from '../recipes';
import type {CRDTImplementation} from './CRDTImplementation';
/**
* The structure of the metadata of this crdt.
*/
interface LWWRegisterMetaData {
timestamp: number;
}
/**
* Function that verifies that the metadata has the correct format.
*
* @param {any} data - The data to check for compatibility
* @returns {boolean}
*/
function isLWWRegisterMetadata(data: any): data is LWWRegisterMetaData {
return data && typeof data.timestamp === 'number';
}
/**
* Implementation of the last writer wins register crdt (LWW-Register)
*
* See paper:
* A comprehensive study of Convergent and Commutative Replicated Data Types
* Marc Shapiro, Nuno PreguiƧa, Carlos Baquero, Marek Zawirski
* p. 19, Section 3.2.1
* https://hal.inria.fr/inria-00555588/document
*/
export class LWWRegister implements CRDTImplementation {
/**
* This function generates the recipe rules for the CRDT metadata
*
* @param {RecipeRule} _rule
* @param {string} _path
* @returns {RecipeRule}
*/
generateRecipeRules(_rule: RecipeRule, _path: string): RecipeRule[] {
return [
{
itemprop: 'timestamp',
itemtype: {type: 'number'}
}
];
}
async generateMetaData(
dataOld: unknown,
dataNew: unknown,
metadataOld: unknown,
rule: Readonly<RecipeRule>
): Promise<LWWRegisterMetaData> {
const serializedOld = dataOld !== undefined && convertValue(rule, dataOld);
const serializedNew = convertValue(rule, dataNew);
if (metadataOld !== undefined && !isLWWRegisterMetadata(metadataOld)) {
throw new Error(`Old metadata has invalid format: ${metadataOld}`);
}
if (metadataOld !== undefined && serializedOld === serializedNew) {
return metadataOld;
} else {
return {
timestamp: Date.now()
};
}
}
async mergeMetaData(
objLatestVersion: unknown,
metadataLatestVersion: unknown,
objToMerge: unknown,
metadataToMerge: unknown,
rule: Readonly<RecipeRule>
): Promise<{metadata: LWWRegisterMetaData; data: unknown}> {
if (metadataLatestVersion !== undefined && !isLWWRegisterMetadata(metadataLatestVersion)) {
throw new Error(`Old medatata has invalid format: ${metadataLatestVersion}`);
}
if (!isLWWRegisterMetadata(metadataToMerge)) {
throw new Error(`Merge metadata has invalid format ${metadataToMerge}`);
}
if (
objLatestVersion === undefined ||
metadataLatestVersion === undefined ||
metadataLatestVersion.timestamp < metadataToMerge.timestamp
) {
return {
metadata: {
timestamp: metadataToMerge.timestamp
},
data: objToMerge
};
} else if (metadataLatestVersion.timestamp > metadataToMerge.timestamp) {
return {
metadata: {
timestamp: metadataLatestVersion.timestamp
},
data: objLatestVersion
};
} else {
const serializedOld = convertValue(rule, objLatestVersion);
const serializedNew = convertValue(rule, objToMerge);
if (serializedOld < serializedNew) {
return {
metadata: {
timestamp: metadataToMerge.timestamp
},
data: objToMerge
};
} else {
return {
metadata: {
timestamp: metadataLatestVersion.timestamp
},
data: objLatestVersion
};
}
}
}
}