UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

178 lines 6.07 kB
import { DataSerializerUtils } from '../DataSerializerUtils'; export const CHANGELOG_METADATA_KEY = Symbol('__changelog__'); /** * Change log for tracking changes on an object */ export class ChangeLog { constructor() { /** * Changes */ this.changes = []; } /** * Reset the log with the assumption that the object has been saved */ reset() { this.changes = []; } /** * Add a change to the log * @param property {string} Property key * @param oldValue {any} Old value * @param newValue {any} New value */ addChange(property, oldValue, newValue) { if (oldValue === newValue) { return; } this.changes.push({ property, oldValue, newValue, date: new Date() }); } /** * Get the latest changes * @returns {Change[]} Latest changes */ getLatestChanges() { // Get the changes per property const changesPerProperty = {}; this.changes.forEach(change => { if (!changesPerProperty[change.property]) { changesPerProperty[change.property] = []; } changesPerProperty[change.property].push(change); }); // Sort the changes by date Object.keys(changesPerProperty).forEach(property => { changesPerProperty[property].sort((a, b) => a.date.getTime() - b.date.getTime()); }); // Filter out changes that end with the same value as the initial state const unchangedProperties = []; Object.keys(changesPerProperty).forEach(property => { const lastIndex = changesPerProperty[property].length - 1; if (changesPerProperty[property][0].oldValue === changesPerProperty[property][lastIndex].newValue) { unchangedProperties.push(property); } }); // Remove the unchanged properties Object.keys(changesPerProperty).forEach(property => { changesPerProperty[property] = changesPerProperty[property].filter(() => !unchangedProperties.includes(property)); }); // Aggregate all changes of each properties const changes = Object.keys(changesPerProperty).map(property => { const lastIndex = changesPerProperty[property].length - 1; const firstChange = changesPerProperty[property][0]; const lastChange = changesPerProperty[property][lastIndex]; if (lastChange) { return { property, oldValue: firstChange.oldValue, newValue: lastChange.newValue, date: lastChange.date }; } return undefined; }).filter(p => p !== undefined); return changes; } /** * Get the deleted properties * @returns {string[]} Deleted properties */ getDeletedProperties() { return this.getLatestChanges().filter(change => change.newValue === undefined).map(change => change.property); } /** * Get the added properties * @returns {string[]} Added properties */ getAddedProperties() { return this.getLatestChanges().filter(change => change.oldValue === undefined).map(change => change.property); } } /** * Get the change log of an object * @param target Target object */ export function getChangeLog(target) { return target[CHANGELOG_METADATA_KEY]; } const IGNORED_TYPES = [Uint8Array, Date]; /** * Create a change log for an object * @param target Target object */ export function createChangeLog(target) { target[CHANGELOG_METADATA_KEY] = new ChangeLog(); // Wrap all data members with a changelog to track deep changes const metadata = DataSerializerUtils.getOwnMetadata(target.constructor); if (metadata) { const watchedProperties = []; metadata.dataMembers.forEach(member => { watchedProperties.push(member.key); if (target[member.key]) { if (Array.isArray(target[member.key])) { target[member.key].forEach(element => { if (element instanceof Object) { element = createChangeLog(element); } }); // Wrap the array in a proxy to track changes target[member.key] = new Proxy(target[member.key], { set: (arr, index, value) => { const oldArray = [...arr]; arr[index] = value; const newArray = [...arr]; target[CHANGELOG_METADATA_KEY].addChange(member.key, oldArray, newArray); return true; } }); } else if (target[member.key] instanceof Map || target[member.key] instanceof Set) { target[member.key].forEach(element => { if (element instanceof Object) { element = createChangeLog(element); } }); // The map itself should also be watched target[member.key] = createChangeLog(target[member.key]); } else if (target[member.key] instanceof Object) { // Only wrap objects that are not ignored if (!IGNORED_TYPES.includes(target[member.key].constructor)) { target[member.key] = createChangeLog(target[member.key]); } } } }); // Wrap the target in a proxy to track changes const proxy = new Proxy(target, { set: (obj, prop, value) => { if (watchedProperties.includes(prop.toString())) { if (obj[prop] !== value) { obj[CHANGELOG_METADATA_KEY].addChange(prop.toString(), obj[prop], value); } obj[prop] = value; } else { // Get the current state of watched properties const currentState = {}; watchedProperties.forEach(watchedProperty => { currentState[watchedProperty] = obj[watchedProperty]; }); obj[prop] = value; // Determine if a setter modified another variable watchedProperties.forEach(watchedProperty => { if (currentState[watchedProperty] !== obj[watchedProperty]) { obj[CHANGELOG_METADATA_KEY].addChange(watchedProperty, currentState[watchedProperty], obj[watchedProperty]); } }); } return true; } }); return proxy; } return target; }