diff --git a/.gitignore b/.gitignore index 95ccaa1..2c4ad41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *sublime* -node_modules/ +node_modules coverage/ +dist diff --git a/package-lock.json b/package-lock.json index bc19537..0b068d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,25 @@ "version": "0.2.0", "license": "MIT", "devDependencies": { + "@types/mocha": "^10.0.1", + "@types/node": "^18.15.0", "istanbul": "^0.4.5", - "mocha": "^10.2.0" + "mocha": "^10.2.0", + "typescript": "^4.9.5" } }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.0.tgz", + "integrity": "sha512-z6nr0TTEOBGkzLGmbypWOGnpSpSIBorEhC4L+4HeQ2iezKCi4f77kyslRwvHeNitymGQ+oFyIWGP96l/DPSV9w==", + "dev": true + }, "node_modules/abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -725,9 +740,9 @@ } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1178,6 +1193,19 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", diff --git a/package.json b/package.json index 21a490e..2f9fb18 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,33 @@ "version": "0.2.0", "description": "A library of Conflict-Free Replicated Data Types for JavaScript.", "type": "module", - "main": "./src/index.js", + "types": "./dist/src/index.d.ts", "repository": { "type": "git", "url": "https://github.com/orbitdb/crdts" }, + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, "bugs": "https://github.com/orbitdb/crdts/issues", "author": "Haad", "homepage": "https://github.com/orbitdb/crdts", @@ -25,8 +47,9 @@ "test": "test" }, "scripts": { - "test": "mocha", - "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha" + "test": "mocha dist/test", + "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha", + "build": "mkdir -p dist/test && cp test/*.js dist/test/ && tsc" }, "localMaintainers": [ "haad ", @@ -34,7 +57,10 @@ "hajamark " ], "devDependencies": { + "@types/mocha": "^10.0.1", + "@types/node": "^18.15.0", "istanbul": "^0.4.5", - "mocha": "^10.2.0" + "mocha": "^10.2.0", + "typescript": "^4.9.5" } } diff --git a/src/2P-Set.js b/src/2P-Set.ts similarity index 76% rename from src/2P-Set.js rename to src/2P-Set.ts index 5225df5..631a2b7 100644 --- a/src/2P-Set.js +++ b/src/2P-Set.ts @@ -13,13 +13,15 @@ import GSet from './G-Set.js' * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document, "3.3.2 2P-Set" */ - export default class TwoPSet extends CRDTSet { + export default class TwoPSet extends CRDTSet { + protected _added: GSet + protected _removed: GSet /** * Create a new TwoPSet instance * @param {[Iterable]} added [Added values] * @param {[Iterable]} removed [Removed values] */ - constructor (added, removed) { + constructor (added: Iterable, removed: Iterable) { super() // We track the operations and state differently // than the base class: use two GSets for operations @@ -32,11 +34,13 @@ import GSet from './G-Set.js' * @override * @return {[Iterator]} [Iterator for values in the Set] */ - values () { - // A value is included in the set if it's present in + values (): IterableIterator { + // A value is included in the set if it's present in // the add set and not present in the remove set. We can // determine this by calculating the difference between // adds and removes. + + // @ts-ignore merge is different between GSet and CmRDTSet. const difference = GSet.difference(this._added, this._removed) return difference.values() } @@ -45,8 +49,10 @@ import GSet from './G-Set.js' * Add a value to the Set * @param {[Any]} value [Value to add to the Set] */ - add (element) { + add (element: V): this { this._added.add(element) + + return this } /** @@ -54,7 +60,7 @@ import GSet from './G-Set.js' * @override * @param {[Any]} element [Value to remove from the Set] */ - remove (element) { + remove (element: V) { // Only add the value to the remove set if it exists in the add set if (this._added.has(element)) { this._removed.add(element) @@ -66,7 +72,8 @@ import GSet from './G-Set.js' * @override * @param {[TwoPSet]} other [Set to merge with] */ - merge (other) { + // @ts-ignore TS2416 We are modifying the signature of CRDTSet here. + merge (other: TwoPSet) { this._added = new GSet(this._added.toArray().concat(other._added.toArray())) this._removed = new GSet(this._removed.toArray().concat(other._removed.toArray())) } @@ -75,7 +82,8 @@ import GSet from './G-Set.js' * TwoPSet as an Object that can be JSON.stringified * @return {[Object]} [Object in the shape of `{ values: { added: [], removed: [] } }`] */ - toJSON () { + // @ts-ignore TS2416 We are modifying the signature of CRDTSet here. + toJSON (): { values: { added: V[], removed: V[] } } { return { values: { added: this._added.toArray(), @@ -89,7 +97,7 @@ import GSet from './G-Set.js' * @param {[Object]} json [Input object to create the GSet from. Needs to be: '{ values: { added: [...], removed: [...] } }'] * @return {[TwoPSet]} [new TwoPSet instance] */ - static from (json) { + static from (json: { values: { added: V[], removed: V[] } }): TwoPSet { return new TwoPSet(json.values.added, json.values.removed) } } diff --git a/src/CmRDT-Set.js b/src/CmRDT-Set.ts similarity index 70% rename from src/CmRDT-Set.js rename to src/CmRDT-Set.ts index 8092711..88dd319 100644 --- a/src/CmRDT-Set.js +++ b/src/CmRDT-Set.ts @@ -4,17 +4,17 @@ 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 + * + * 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: + * 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 @@ -24,30 +24,37 @@ import { OperationTuple3 } from './utils.js' * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document */ -export default class CmRDTSet extends Set { +export default class CmRDTSet extends Set { + // Internal cache for tracking which values have been added to the set + protected _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 + protected _operations: OperationTuple3[] + + protected _options: Partial<{ compareFunc: (a: T, b: T) => boolean }> = {} + /** * Create a new CmRDTSet instance * @override * - * The constructor should never be used directly + * 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) { + constructor (iterable?: Iterable<{ value: V, added: T[], removed: T[] }>, options?: Partial<{ compareFunc: (a: T, b: T) => boolean }>) { 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) : [] + + this._operations = iterable ? [...iterable].map(OperationTuple3.from) : [] + // Internal options - this._options = options || {} + this._options = options ?? {} } /** @@ -55,9 +62,9 @@ export default class CmRDTSet extends 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 + values (): IterableIterator { + const shouldIncludeValue = (e: OperationTuple3) => this._resolveValueState(e.added, e.removed, this._options.compareFunc) + const getValue = (e: OperationTuple3) => 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 @@ -72,7 +79,7 @@ export default class CmRDTSet extends Set { * @param {[Any]} value [Value to look for] * @return {Boolean} [True if value is in the Set, false if not] */ - has (value) { + has (value: V): boolean { return new Set(this.values()).has(value) } @@ -81,9 +88,9 @@ export default class CmRDTSet extends Set { * @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) + hasAll (values: V[]): boolean { + const contains = (e: V) => this.has(e) + return values.every(contains) } /** @@ -94,20 +101,23 @@ export default class CmRDTSet extends Set { * 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) { + // @ts-ignore TS2416 We are modifying the signature of Set here. + add (value: V, tag: T): this { // 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) + const addOperation = OperationTuple3.create(value, [tag]) 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)) } + + return this } /** @@ -122,18 +132,18 @@ export default class CmRDTSet extends 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)) - } + remove (value: V, tag: T): void { + // 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) { + merge (other: CmRDTSet): void { other._operations.forEach(operation => { const value = operation.value if (!this._values.has(value)) { @@ -155,13 +165,13 @@ export default class CmRDTSet extends Set { * CmRDT-Set as an Object that can be JSON.stringified * @return {[Object]} [Object in the shape of `{ values: [ { value: , added: [], removed: [] } ] }`] */ - toJSON () { + toJSON (): { values: { value: V, added: T[], removed: T[] }[] } { const values = this._operations.map(e => { return { value: e.value, added: Array.from(e.added), removed: Array.from(e.removed), - } + } }) return { values: values } } @@ -170,7 +180,7 @@ export default class CmRDTSet extends Set { * Create an Array of the values of this Set * @return {[Array]} [Values of this Set as an Array] */ - toArray () { + toArray (): V[] { return Array.from(this.values()) } @@ -179,29 +189,29 @@ export default class CmRDTSet extends Set { * @param {[type]} other [Set to compare] * @return {Boolean} [True if this Set is the same as the other Set] */ - isEqual (other) { + isEqual (other: CmRDTSet): boolean { 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) { + protected _resolveValueState (added: Set, removed: Set, compareFunc?: (a: T, b: T) => boolean): boolean { // By default, if there's an add operation present, - // and there are no remove operations, we include + // and there are no remove operations, we include // the value in the set return added.size > 0 && removed.size === 0 } @@ -210,9 +220,9 @@ export default class CmRDTSet extends Set { * Add a value to the internal cache * @param {[OperationTuple3]} operationTuple3 [Tuple3(value, addedTags, removedTags)] */ - _applyOperation (operationTuple3) { + protected _applyOperation (operationTuple3: OperationTuple3) { this._operations.push(operationTuple3) - this._values.add(operationTuple3.value) + this._values.add(operationTuple3.value) } /** @@ -224,16 +234,17 @@ export default class CmRDTSet extends 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 = [] + protected _findOperationsFor (value: V) { + let operations: OperationTuple3[] = [] if (this._values.has(value)) { - const isForValue = e => e.value === value - const notNull = e => e !== undefined - operations = [this._operations.find(isForValue)].filter(notNull) + const isForValue = (e: OperationTuple3) => e.value === value + const notNull = (e?: OperationTuple3) => e !== undefined + + operations = [this._operations.find(isForValue)].filter(notNull) as OperationTuple3[] } return operations } @@ -243,34 +254,33 @@ export default class CmRDTSet extends Set { * @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) + static from (json: { values: { value: V, added: T[], removed: T[] }[] }): CmRDTSet { + return new CmRDTSet(json.values) } /** * Check if two Set are equal * - * Two Set are equal if they both contain exactly + * 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()) + static isEqual (a: CmRDTSet, b: CmRDTSet) { + 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) + static difference (a: CmRDTSet, b: CmRDTSet) { + const otherDoesntInclude = (x: V) => !b.has(x) const difference = new Set([...a.values()].filter(otherDoesntInclude)) return difference } diff --git a/src/G-Counter.js b/src/G-Counter.ts similarity index 64% rename from src/G-Counter.js rename to src/G-Counter.ts index 7825f57..818c769 100644 --- a/src/G-Counter.js +++ b/src/G-Counter.ts @@ -1,38 +1,41 @@ import { deepEqual } from './utils.js' -const sum = (acc, val) => acc + val +const sum = (acc: number, val: number) => acc + val /** * G-Counter * * Operation-based Increment-Only Counter CRDT * - * Sources: + * Sources: * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document, "3.1.1 Op-based counter and 3.1.2 State-based increment-only Counter (G-Counter)" */ export default class GCounter { - constructor (id, counter) { + readonly id: string; + private readonly _counters: Record; + + constructor (id: string, counter: Record) { this.id = id this._counters = counter ? counter : {} this._counters[this.id] = this._counters[this.id] ? this._counters[this.id] : 0 } - get value () { + get value (): number { return Object.values(this._counters).reduce(sum, 0) } - increment (amount) { - if (amount && amount < 1) + increment (amount?: number): void { + if (amount && amount < 1) return - if (amount === undefined || amount === null) + if (amount == null) amount = 1 this._counters[this.id] = this._counters[this.id] + amount } - merge (other) { + merge (other: GCounter) { // Go through each counter in the other counter Object.entries(other._counters).forEach(([id, value]) => { // Take the maximum of the counter value we have or the counter value they have @@ -40,22 +43,22 @@ export default class GCounter { }) } - toJSON () { - return { - id: this.id, - counters: this._counters + toJSON (): { id: string, counters: Record } { + return { + id: this.id, + counters: this._counters } } - isEqual (other) { + isEqual (other: GCounter) { return GCounter.isEqual(this, other) } - static from (json) { + static from (json: { id: string, counters: Record }) { return new GCounter(json.id, json.counters) } - static isEqual (a, b) { + static isEqual (a: GCounter, b: GCounter) { if(a.id !== b.id) return false diff --git a/src/G-Set.js b/src/G-Set.ts similarity index 75% rename from src/G-Set.js rename to src/G-Set.ts index 40ba0ac..3e67bc9 100644 --- a/src/G-Set.js +++ b/src/G-Set.ts @@ -7,23 +7,23 @@ import CRDTSet from './CmRDT-Set.js' * * G stands for "Grow-Only" which means that values can only * ever be added to the set, they can never be removed. - * + * * See base class CmRDT-Set.js for the rest of the API * https://github.com/orbitdb/crdts/blob/master/src/CmRDT-Set.js * * Used by: * 2P-Set - https://github.com/orbitdb/crdts/blob/master/src/2P-Set.js * - * Sources: + * Sources: * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document, "3.3.1 Grow-Only Set (G-Set)" */ -export default class GSet extends CRDTSet { +export default class GSet extends CRDTSet { /** * Create a G-Set CRDT instance * @param {[Iterable]} iterable [Opetional Iterable object (eg. Array, Set) to create the GSet from] */ - constructor (iterable) { + constructor (iterable: Iterable) { super() this._values = new Set(iterable) } @@ -32,7 +32,7 @@ export default class GSet extends CRDTSet { * Return all values added to the Set * @return {[Iterator]} [Iterator for values in the Set] */ - values () { + values (): IterableIterator { return this._values.values() } @@ -44,14 +44,15 @@ export default class GSet extends CRDTSet { * * @param {[Any]} value [Value to add to the Set] */ - add (value) { + add (value: V): this { this._values.add(value) + return this } // G-Set doesn't allow removal of values, throw an error // Including this to satisfy normal Set API in case the user // accidentally calls remove on GSet - remove (value) { + remove (value: V) { throw new Error(`G-Set doesn't allow removing values`) } @@ -59,17 +60,19 @@ export default class GSet extends CRDTSet { * Merge another GSet to this GSet * @param {[GSet]} other [GSet to merge with] */ - merge (other) { + // @ts-ignore TS2416 We are modifying the signature of CRDTSet here. + merge (other: GSet): void { // Merge values of other set with this set - this._values = new Set([...this._values, ...other._values]) + this._values = new Set([...this._values, ...other._values]) } /** * GSet as an Object that can be JSON.stringified * @return {[Object]} [Object in the shape of `{ values: [] }`] */ - toJSON () { - return { + // @ts-ignore TS2416 We are modifying the signature of CRDTSet here. + toJSON (): { values: V[] } { + return { values: this.toArray(), } } @@ -79,7 +82,8 @@ export default class GSet extends CRDTSet { * @param {[Object]} json [Input object to create the GSet from. Needs to be: '{ values: [] }'] * @return {[GSet]} [new GSet instance] */ - static from (json) { - return new GSet(json.values) + + static from (json: { values: V[] }) { + return new GSet(json.values) } } diff --git a/src/LWW-Set.js b/src/LWW-Set.ts similarity index 72% rename from src/LWW-Set.js rename to src/LWW-Set.ts index 4e0951b..7c01023 100644 --- a/src/LWW-Set.js +++ b/src/LWW-Set.ts @@ -12,34 +12,33 @@ import CRDTSet from './CmRDT-Set.js' * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document, "Figure 8: LWW-Set (state-based)" */ -export default class LWWSet extends CRDTSet { +export default class LWWSet extends CRDTSet { /** * @override - * + * * _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) { - // Sort both sets with the given comparison function - // or use "distance" sort by default - compareFunc = compareFunc ? compareFunc : (a, b) => (a || 0) - (b || 0) - const sortedAdded = Array.from(added).sort(compareFunc).reverse() - const sortedRemoved = Array.from(removed).sort(compareFunc).reverse() + // @ts-ignore TS2416 We are modifying the signature of CRDTSet here. + _resolveValueState (added: Set, removed: Set, compareFunc?: (a: T, b: T) => number) { + const comp = compareFunc ? compareFunc : (a: T, b: T) => (a as number || 0) - (b as number || 0) + const sortedAdded = Array.from(added).sort(comp).reverse() + const sortedRemoved = Array.from(removed).sort(comp).reverse() // If the latest add operation is greater or equal than latest remove operation, // we include it in the state - return compareFunc(sortedAdded[0], sortedRemoved[0]) > -1 + return comp(sortedAdded[0], sortedRemoved[0]) > -1 } /** @@ -47,7 +46,7 @@ export default class LWWSet extends CRDTSet { * @param {[Object]} json [Input object to create the LWWSet from. Needs to be: '{ values: [] }'] * @return {[LWWSet]} [new LWWSet instance] */ - static from (json) { + static from (json: { values: { value: V, added: T[], removed: T[] }[] }): LWWSet { return new LWWSet(json.values) } } diff --git a/src/OR-Set.js b/src/OR-Set.ts similarity index 82% rename from src/OR-Set.js rename to src/OR-Set.ts index 2c12b78..d692f73 100644 --- a/src/OR-Set.js +++ b/src/OR-Set.ts @@ -12,57 +12,56 @@ import CRDTSet from './CmRDT-Set.js' * "A comprehensive study of Convergent and Commutative Replicated Data Types" * http://hal.upmc.fr/inria-00555588/document, "3.3.5 Observed-Remove Set (OR-Set)" */ -export default class ORSet extends CRDTSet { +export default class ORSet extends CRDTSet { /** * @override - * + * * Remove a value from the Set * * Overriding the remove functionality for OR-Set, so that we * have the Observed-remove mechanics: when a remove operation * is executed, we add all the known add operation tags to the * removed tags allowing us to exclude the value from the set - * in _resolveState() if all given add tags are present in + * in _resolveState() if all given add tags are present in * remove tags. * * @param {[Any]} value [Value to remove from the Set] * @param {[Any]} tag [Optional tag for this remove operation, eg. a clock] */ - remove (value) { + remove (value: V): void { // Add all observed (known) add tags to the removed tags - const removeObserved = e => e.removed = new Set([...e.added, ...e.removed]) // Create a remove operation for the value if it exists - this._findOperationsFor(value).map(removeObserved) + this._findOperationsFor(value).map(e => [...e.added, ...e.removed].map(v => e.removed.add(v))); } /** * @override - * + * * _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) { + _resolveValueState (added: Set, removed: Set, compareFunc: (a: T, b: T) => boolean): boolean { // Check if a tag is included in the remove set - const hasMatchingRemoveOperation = addTag => { - // Check if remove tags includes the add tag, ie. check for + const hasMatchingRemoveOperation = (addTag: T) => { + // Check if remove tags includes the add tag, ie. check for // equality for the tags using a provided comparison function if (compareFunc) { return !Array.from(removed).some(removeTag => compareFunc(removeTag, addTag)) } - // If remove set doesn't have the tag, + // If remove set doesn't have the tag, // return true to include the value in the state return !removed.has(addTag) } @@ -76,7 +75,7 @@ export default class ORSet extends CRDTSet { * @param {[Object]} json [Input object to create the ORSet from. Needs to be: '{ values: [] }'] * @return {[ORSet]} [new ORSet instance] */ - static from (json) { + static from (json: { values: { value: V, added: T[], removed: T[] }[] }): ORSet { return new ORSet(json.values) } } diff --git a/src/PN-Counter.js b/src/PN-Counter.js deleted file mode 100644 index dea1516..0000000 --- a/src/PN-Counter.js +++ /dev/null @@ -1,48 +0,0 @@ -import GCounter from '../src/G-Counter.js' - -const isGCounter = (obj) => obj && obj instanceof GCounter - -export default class PNCounter { - constructor (id, pCounters, nCounters) { - this.id = id - this.p = isGCounter(pCounters) ? pCounters : new GCounter(id, pCounters) - this.n = isGCounter(nCounters) ? nCounters : new GCounter(id, nCounters) - } - - get value() { - return this.p.value - this.n.value - } - - increment (amount) { - this.p.increment(amount) - } - - decrement (amount) { - this.n.increment(amount) - } - - merge (other) { - this.p.merge(other.p) - this.n.merge(other.n) - } - - toJSON () { - return { - id: this.id, - p: this.p._counters, - n: this.n._counters - } - } - - isEqual (other) { - return PNCounter.isEqual(this, other) - } - - static from (json) { - return new PNCounter(json.id, json.p, json.n) - } - - static isEqual (a, b) { - return GCounter.isEqual(a.p, b.p) && GCounter.isEqual(a.n, b.n) - } -} diff --git a/src/PN-Counter.ts b/src/PN-Counter.ts new file mode 100644 index 0000000..96d40cc --- /dev/null +++ b/src/PN-Counter.ts @@ -0,0 +1,58 @@ +import GCounter from '../src/G-Counter.js' + +export default class PNCounter { + readonly id: string + readonly p: GCounter + readonly n: GCounter + + constructor (id: string, pCounters: Record | GCounter, nCounters: Record | GCounter) { + this.id = id + + if (pCounters instanceof GCounter) + this.p = pCounters + else + this.p = new GCounter(id, pCounters) + + if (nCounters instanceof GCounter) + this.n = nCounters + else + this.n = new GCounter(id, nCounters) + } + + get value(): number { + return this.p.value - this.n.value + } + + increment (amount: number): void { + this.p.increment(amount) + } + + decrement (amount: number): void { + this.n.increment(amount) + } + + merge (other: PNCounter) { + this.p.merge(other.p) + this.n.merge(other.n) + } + + toJSON (): { id: string, p: Record, n: Record } { + return { + id: this.id, + p: this.p.toJSON().counters, + n: this.n.toJSON().counters + } + } + + isEqual (other: PNCounter): boolean { + return PNCounter.isEqual(this, other) + } + + static from (json: { id: string, p: Record, n: Record }): PNCounter { + return new PNCounter(json.id, json.p, json.n) + } + + static isEqual (a: PNCounter, b: PNCounter): boolean { + return GCounter.isEqual(a.p, b.p) && GCounter.isEqual(a.n, b.n) + } +} diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index bc9b929..0000000 --- a/src/utils.js +++ /dev/null @@ -1,36 +0,0 @@ -const deepEqual = (a, b) => { - const propsA = Object.getOwnPropertyNames(a) - const propsB = Object.getOwnPropertyNames(b) - - if(propsA.length !== propsB.length) - return false - - for(let i = 0; i < propsA.length; i ++) { - const prop = propsA[i] - if(a[prop] !== b[prop]) - return false - } - - return true -} - -class OperationTuple3 { - constructor (value, added, removed) { - this.value = value - this.added = new Set(added) - this.removed = new Set(removed) - } - - static create (value, added, removed) { - return new OperationTuple3(value, added, removed) - } - - static from (json) { - return OperationTuple3.create(json.value, json.added, json.removed) - } -} - -export { - deepEqual, - OperationTuple3 -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..9680191 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,35 @@ +export const deepEqual = (a: Record, b: Record): boolean => { + const propsA = Object.getOwnPropertyNames(a) + const propsB = Object.getOwnPropertyNames(b) + + if(propsA.length !== propsB.length) + return false + + for(let i = 0; i < propsA.length; i ++) { + const prop = propsA[i] + if(a[prop] !== b[prop]) + return false + } + + return true +} + +export class OperationTuple3 { + readonly value: V + readonly added: Set + readonly removed: Set + + constructor (value: V, added?: Iterable, removed?: Iterable) { + this.value = value + this.added = new Set(added) + this.removed = new Set(removed) + } + + static create (value: V, added?: Iterable, removed?: Iterable): OperationTuple3 { + return new OperationTuple3(value, added, removed) + } + + static from (json: { value: V, added: Iterable, removed: Iterable }): OperationTuple3 { + return OperationTuple3.create(json.value, json.added, json.removed) + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a1ec9fc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "strict": true, + "outDir": "dist", + "allowJs": true, + "checkJs": true, + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2021", "ES2021.Promise", "ES2021.String", "ES2020.BigInt", "DOM", "DOM.Iterable"], + "noEmit": false, + "noEmitOnError": true, + "emitDeclarationOnly": false, + "declaration": true, + "declarationMap": true, + "incremental": true, + "composite": true, + "isolatedModules": true, + "removeComments": false, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "node", + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "importsNotUsedAsValues": "error", + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "stripInternal": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +}