Source: crdt/LWWRegister.ts

/**
 * @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
                };
            }
        }
    }
}