UNPKG

crdts

Version:

A library of Conflict-Free Replicated Data Types for JavaScript.

278 lines (261 loc) 9.51 kB
import { OperationTuple3 } from './utils.js' /** * CmRDT-Set * * Base Class for Operation-Based Set CRDT. Provides a Set interface. * * Operations are described as: * * Operation = Tuple3(value : Any, added : Set, removed : Set) * * This class is meant to be used as a base class for * Operation-Based CRDTs that can be derived from Set * semantics and which calculate the state (values) * based on a set of operations. * * Used by: * G-Set - https://github.com/orbitdb/crdts/blob/master/src/G-Set.js * OR-Set - https://github.com/orbitdb/crdts/blob/master/src/OR-Set.js * 2P-Set - https://github.com/orbitdb/crdts/blob/master/src/2P-Set.js * LWW-Set - https://github.com/orbitdb/crdts/blob/master/LWW-Set.js * * Sources: * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document */ export default class CmRDTSet extends Set { /** * Create a new CmRDTSet instance * @override * * The constructor should never be used directly * but rather via `super()` call in the constructor of * the class that inherits from CmRDTSet * * @param {[Iterable]} iterable [Opetional Iterable object (eg. Array, Set) to create the Set from] * @param {[Object]} options [Options to pass to the Set. Currently supported: `{ compareFunc: (a, b) => true|false }`] */ constructor (iterable, options) { super() // Internal cache for tracking which values have been added to the set this._values = new Set() // List of operations (adds or removes of a value) for this set as // Operation : Tuple3(value : Any, added : Set, removed : Set) // added and removed can be any value, eg. it can be used to store // timestamps/clocks for each operation in order to determine if // a value is in the set this._operations = iterable ? iterable.map(OperationTuple3.from) : [] // Internal options this._options = options || {} } /** * Return the values in the Set * @override * @return {[Set]} [Values in this set] */ values () { const shouldIncludeValue = e => this._resolveValueState(e.added, e.removed, this._options.compareFunc) const getValue = e => e.value // Filter out values that should not be in this set // by using the _resolveValueState() function to determine // if the value should be present const state = this._operations .filter(shouldIncludeValue) .map(getValue) return new Set(state).values() } /** * Check if this Set has a value * @param {[Any]} value [Value to look for] * @return {Boolean} [True if value is in the Set, false if not] */ has (value) { return new Set(this.values()).has(value) } /** * Check if this Set has all values of an input array * @param {[Array]} values [Values that should be in the Set] * @return {Boolean} [True if all values are in the Set, false if not] */ hasAll (values) { const contains = e => this.has(e) return values.every(contains) } /** * Add a value to the Set * @override * * Optionally, a "tag" can be given for the add operation, * for example the tag can be a clock or other identifier * that can be used to determine together with remove operations, * whether a value is included in the Set * * @param {[Any]} value [Value to add to the Set] * @param {[Any]} tag [Optional tag for this add operation, eg. a clock] */ add (value, tag) { // If the value is not in the set yet if (!this._values.has(value)) { // Create an operation for the value and apply it to this set const addOperation = OperationTuple3.create(value, [tag], null) this._applyOperation(addOperation) } else { // If the value is in the set, add a tag to its added set this._findOperationsFor(value).map(val => val.added.add(tag)) } } /** * Remove a value from the Set * @override * * Optionally, a "tag" can be given for the remove operation, * for example the tag can be a clock or other identifier * that can be used to determine together with add operations, * whether a value is included in the Set * * @param {[Any]} value [Value to remove from the Set] * @param {[Any]} tag [Optional tag for this remove operation, eg. a clock] */ remove (value, tag) { // Add a remove tag to the value's removed set, and only // apply the remove operation if the value was added previously this._findOperationsFor(value).map(e => e.removed.add(tag)) } /** * Merge the Set with another Set * @override * @param {[CRDTSet]} other [Set to merge with] */ merge (other) { other._operations.forEach(operation => { const value = operation.value if (!this._values.has(value)) { // If we don't have the value yet, add it with all tags from other's operation const op = OperationTuple3.create(value, operation.added, operation.removed) this._applyOperation(op) } else { // If this set has the value this._findOperationsFor(value).map(op => { // Add all add and remove tags from other's operations to value in this set operation.added.forEach(e => op.added.add(e)) operation.removed.forEach(e => op.removed.add(e)) }) } }) } /** * CmRDT-Set as an Object that can be JSON.stringified * @return {[Object]} [Object in the shape of `{ values: [ { value: <value>, added: [<tags>], removed: [<tags>] } ] }`] */ toJSON () { const values = this._operations.map(e => { return { value: e.value, added: Array.from(e.added), removed: Array.from(e.removed), } }) return { values: values } } /** * Create an Array of the values of this Set * @return {[Array]} [Values of this Set as an Array] */ toArray () { return Array.from(this.values()) } /** * Check if this Set equal another Set * @param {[type]} other [Set to compare] * @return {Boolean} [True if this Set is the same as the other Set] */ isEqual (other) { return CmRDTSet.isEqual(this, other) } /** * _resolveValueState function is used to determine if an element is present in a Set. * * It receives a Set of add tags and a Set of remove tags for an element as arguments. * It returns true if an element should be included in the state and false if not. * * Overwriting this function gives us the ability to compare add/remove operations * of a particular element (value) in the set and determine if the value should be * included in the set or not. The function gets called once per element and returning * true will include the value in the set and returning false will exclude it from the set. * * @param {[type]} added [Set of added elements] * @param {[type]} removed [Set of removed elements] * @param {[type]} compareFunc [Comparison function to compare elements with] * @return {[type]} [true if element should be included in the current state] */ _resolveValueState (added, removed, compareFunc) { // By default, if there's an add operation present, // and there are no remove operations, we include // the value in the set return added.size > 0 && removed.size === 0 } /** * Add a value to the internal cache * @param {[OperationTuple3]} operationTuple3 [Tuple3(value, addedTags, removedTags)] */ _applyOperation (operationTuple3) { this._operations.push(operationTuple3) this._values.add(operationTuple3.value) } /** * Find a value and its operations from this set's internal cache * @private * * Returns a value and all its operations as a OperationTuple3: * Operation : Tuple3(value : Any, added : Set, removed : Set) * * Where 'value' is the value in the set, 'added' is all add operations * and 'removed' are all remove operations for that value. * * @param {[Any]} value [Value to find] * @return {[Any]} [Value if found, undefined if value was not found] */ _findOperationsFor (value) { let operations = [] if (this._values.has(value)) { const isForValue = e => e.value === value const notNull = e => e !== undefined operations = [this._operations.find(isForValue)].filter(notNull) } return operations } /** * Create Set from a json object * @param {[Object]} json [Input object to create the Set from. Needs to be: '{ values: [] }'] * @return {[Set]} [new Set instance] */ static from (json) { return new CmRDTSet(json.values) } /** * Check if two Set are equal * * Two Set are equal if they both contain exactly * the same values. * * @param {[Set]} a [Set to compare] * @param {[Set]} b [Set to compare] * @return {Boolean} [True input Set are the same] */ static isEqual (a, b) { return (a.toArray().length === b.toArray().length) && a.hasAll(b.toArray()) } /** * Return the difference between the values of two Sets * * @param {[Set]} a [First Set] * @param {[Set]} b [Second Set] * @return {[Set]} [Set of values that are in Set A but not in Set B] */ static difference (a, b) { const otherDoesntInclude = x => !b.has(x) const difference = new Set([...a.values()].filter(otherDoesntInclude)) return difference } }