├── .gitignore ├── src ├── index.ts ├── storage │ ├── index.ts │ ├── Atom.ts │ ├── AtomList.ts │ └── ArrayAtomList.ts ├── idents │ ├── index.ts │ ├── Segment.ts │ ├── IdentGenerator.ts │ ├── IdentSet.ts │ ├── Ident.ts │ └── LSEQIdentGenerator.ts ├── Op.ts └── KSeq.ts ├── typings ├── main.d.ts ├── browser.d.ts ├── main │ └── ambient │ │ ├── mocha │ │ └── index.d.ts │ │ └── chai │ │ └── index.d.ts └── browser │ └── ambient │ ├── mocha │ └── index.d.ts │ └── chai │ └── index.d.ts ├── .vscode ├── tasks.json └── launch.json ├── typings.json ├── tsconfig.json ├── package.json ├── README.md ├── test ├── ArrayAtomList.tests.ts ├── IdentSet.tests.ts ├── Ident.tests.ts └── KSeq.tests.ts └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KSeq'; 2 | export * from './Op'; 3 | -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ArrayAtomList'; 2 | export * from './Atom'; 3 | export * from './AtomList'; 4 | -------------------------------------------------------------------------------- /typings/main.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /typings/browser.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "tsc", 4 | "isShellCommand": true, 5 | "showOutput": "silent", 6 | "args": ["-p", "."], 7 | "problemMatcher": "$tsc" 8 | } 9 | -------------------------------------------------------------------------------- /src/idents/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Ident'; 2 | export * from './IdentGenerator'; 3 | export * from './IdentSet'; 4 | export * from './LSEQIdentGenerator'; 5 | export * from './Segment'; 6 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "chai": "registry:dt/chai#3.4.0+20160317120654", 4 | "mocha": "registry:dt/mocha#2.2.5+20160317120654" 5 | }, 6 | "dependencies": {} 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "sourceMap": true 7 | }, 8 | "files": [ 9 | "typings/main.d.ts", 10 | "src/index.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kseq", 3 | "version": "0.1.0", 4 | "description": "An ordered sequence CRDT based on Logoot/LSEQ", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "chai": "^3.5.0", 10 | "mocha": "^2.4.5", 11 | "ts-node": "^0.7.1", 12 | "typescript": "^1.8.7" 13 | }, 14 | "scripts": { 15 | "test": "node_modules/.bin/mocha --compilers ts:ts-node/register test/**/*.ts" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/storage/Atom.ts: -------------------------------------------------------------------------------- 1 | import {Ident} from '../idents'; 2 | 3 | /** 4 | * A value stored in the KSeq, with its unique identifier. 5 | */ 6 | export interface Atom { 7 | 8 | /** 9 | * The atom's unique identifier. 10 | */ 11 | id: Ident 12 | 13 | /** 14 | * The atom's value. 15 | */ 16 | value: T 17 | 18 | } 19 | 20 | /** 21 | * Creates a new Atom. 22 | * @param id The atom's unique identifier. 23 | * @param value The atom's value. 24 | * @returns An instance of Atom. 25 | */ 26 | export function Atom(id: Ident, value: T): Atom { 27 | return {id, value}; 28 | } 29 | -------------------------------------------------------------------------------- /src/idents/Segment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A segment in an Ident's path. 3 | */ 4 | export interface Segment { 5 | 6 | /** 7 | * The numeric value for the Segment. 8 | */ 9 | digit: number 10 | 11 | /** 12 | * The replica identifier for the Segment. 13 | */ 14 | replica: string 15 | 16 | } 17 | 18 | /** 19 | * Creates a new path segment. 20 | * @param digit The digit of the Segment. 21 | * @param replica The replica identifier for the Segment. 22 | * @returns The created Segment. 23 | */ 24 | export function Segment(digit: number, replica: string): Segment { 25 | return {digit, replica}; 26 | } 27 | -------------------------------------------------------------------------------- /src/idents/IdentGenerator.ts: -------------------------------------------------------------------------------- 1 | import {Ident} from './Ident'; 2 | 3 | /** 4 | * Creates Idents using an algorithm that guarantees that the application of 5 | * operations will be associative, commutative, and idempotent. 6 | */ 7 | export interface IdentGenerator { 8 | 9 | /** 10 | * Creates a new Ident whose value lies somewhere between two other Idents. 11 | * @param name The unique replica name that is generating the Ident. 12 | * @param time The local logical time for the replica. 13 | * @param before The Ident that should come directly before the new Ident. 14 | * @param before The Ident that should come directly after the new Ident. 15 | * @returns The newly-generated Ident. 16 | */ 17 | getIdent(name: string, time: number, before: Ident, after: Ident): Ident 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Unit Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/.bin/mocha", 9 | "stopOnEntry": false, 10 | "args": ["--require", "ts-node/register", "test/**/*.ts"], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": true, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/idents/IdentSet.ts: -------------------------------------------------------------------------------- 1 | import {Ident} from './Ident'; 2 | 3 | /** 4 | * A set of Idents. 5 | */ 6 | export class IdentSet { 7 | 8 | private entries: { [ident: string]: boolean }; 9 | 10 | /** 11 | * Creates a new instance of IdentSet. 12 | * @params idents An array of (possibly serialized) Idents to add. 13 | * @returns An instance of IdentSet. 14 | */ 15 | constructor(idents?: Array) { 16 | this.entries = {}; 17 | if (idents) { 18 | idents.forEach((ident) => this.add(ident)); 19 | } 20 | } 21 | 22 | /** 23 | * Gets the cardinality of the set. 24 | * @returns The number of idents in the set. 25 | */ 26 | size(): number { 27 | return Object.keys(this.entries).length; 28 | } 29 | 30 | /** 31 | * Adds the specified Ident to the set. 32 | * @param ident The (possibly serialized) Ident to add. 33 | */ 34 | add(ident: Ident|string) { 35 | this.entries[ident.toString()] = true; 36 | } 37 | 38 | /** 39 | * Determines whether the specified Ident is in the set. 40 | * @param ident The (possibly serialized) Ident in question. 41 | * @returns True if the ident is in the set, otherwise false. 42 | */ 43 | has(ident: Ident|string) { 44 | return !!this.entries[ident.toString()]; 45 | } 46 | 47 | /** 48 | * Removes the specified Ident from the set. 49 | * @param ident The (possibly serialized) Ident to remove. 50 | */ 51 | remove(ident: Ident|string) { 52 | delete this.entries[ident.toString()]; 53 | } 54 | 55 | /** 56 | * Converts the IdentSet to a lightweight representation suitable 57 | * for serialization. 58 | * @returns An array of serialized idents contained in the set. 59 | */ 60 | toJSON() { 61 | return Object.keys(this.entries); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/storage/AtomList.ts: -------------------------------------------------------------------------------- 1 | import {Atom} from './Atom'; 2 | import {Ident} from '../idents'; 3 | 4 | /** 5 | * A sorted list of Atoms, used as backing storage by KSeq. 6 | */ 7 | export interface AtomList { 8 | 9 | /** 10 | * Returns the number of atoms currently stored. 11 | * @returns The number of atoms. 12 | */ 13 | size(): number 14 | 15 | /** 16 | * Gets the atom at the specified position in the sorted list. 17 | * @param pos The desired position. 18 | * @returns The atom at the specified position. 19 | */ 20 | get(pos: number): Atom 21 | 22 | /** 23 | * Adds the specified value with the specified Ident to the list 24 | * at the correct position. 25 | * @param id The identifier for the value. 26 | * @param value The value to add. 27 | * @returns The position at which the value was inserted, 28 | * or -1 if an atom with the specified ident already exists. 29 | */ 30 | add(id: Ident, value: T): number 31 | 32 | /** 33 | * Removes the atom with the specified Ident from the list. 34 | * @param id The identifier of the atom to remove. 35 | * @returns The position of the removed atom, 36 | * or -1 if no atom with the specified identifier exists. 37 | */ 38 | remove(id: Ident): number 39 | 40 | /** 41 | * Gets the index of the atom with the specified identifier. 42 | * @param id The desired identifier. 43 | * @returns The position of the atom with the specified identifer, 44 | * or -1 if no atom with the identifier exists. 45 | */ 46 | indexOf(id: Ident): number 47 | 48 | /** 49 | * Applies a function to each of the atoms in the list. 50 | * @param func The function to apply. 51 | */ 52 | forEach(func: { (atom: Atom): void }): void 53 | 54 | /** 55 | * Applies a transformation function to each of the atoms in the list. 56 | * @param func The transformation function to apply. 57 | * @returns An array containing the results of the function calls. 58 | */ 59 | map(func: { (atom: Atom): R }): R[] 60 | 61 | /** 62 | * Converts the Storage to an array. 63 | * @returns An array representation of the atoms in the list. 64 | */ 65 | toArray(): Atom[] 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/storage/ArrayAtomList.ts: -------------------------------------------------------------------------------- 1 | import {Atom} from './Atom'; 2 | import {AtomList} from './AtomList'; 3 | import {Ident} from '../idents'; 4 | 5 | /** 6 | * An implementation of AtomList that uses a binary insertion sort over 7 | * an array to track a sorted list of atoms. 8 | */ 9 | export class ArrayAtomList implements AtomList { 10 | 11 | private atoms: Atom[] 12 | 13 | /** 14 | * Creates an instange of ArrayAtomList. 15 | */ 16 | constructor() { 17 | this.atoms = []; 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | size(): number { 24 | return this.atoms.length; 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | get(pos: number): Atom { 31 | return this.atoms[pos]; 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | add(id: Ident, value: T): number { 38 | let pos = this.bisectRight(id); 39 | let existing = this.get(pos - 1); 40 | if (existing && id.compare(existing.id) == 0) { 41 | return -1; 42 | } 43 | let atom = Atom(id, value); 44 | this.atoms.splice(pos, 0, atom); 45 | return pos; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | remove(id: Ident): number { 52 | let pos = this.indexOf(id); 53 | if (pos >= 0) { 54 | this.atoms.splice(pos, 1); 55 | return pos; 56 | } 57 | return -1; 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | indexOf(id: Ident): number { 64 | let pos = this.bisectLeft(id); 65 | if (pos !== this.atoms.length && this.atoms[pos].id.compare(id) == 0) { 66 | return pos; 67 | } 68 | else { 69 | return -1; 70 | } 71 | } 72 | 73 | /** 74 | * @inheritdoc 75 | */ 76 | forEach(func: { (atom: Atom): void }): void { 77 | this.atoms.forEach(func); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | map(func: { (atom: Atom): R }): R[] { 84 | return this.atoms.map(func); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | toArray(): Atom[] { 91 | return this.atoms.slice(0); 92 | } 93 | 94 | /** 95 | * A binary search that finds the leftmost position of the atom with the 96 | * specified identifier (if it exists), or the position at which the atom 97 | * would be (if it does not exist). 98 | * @param id The desired identifier. 99 | * @returns The correct position. 100 | */ 101 | private bisectLeft(id: Ident): number { 102 | let min = 0; 103 | let max = this.atoms.length; 104 | 105 | while (min < max) { 106 | let curr = Math.floor((min + max) / 2); 107 | if (this.atoms[curr].id.compare(id) < 0) { 108 | min = curr + 1; 109 | } 110 | else { 111 | max = curr; 112 | } 113 | } 114 | 115 | return min; 116 | } 117 | 118 | /** 119 | * A binary search that finds the position at which an atom with the 120 | * specified identifier should be inserted. 121 | * @param id The desired identifier. 122 | * @returns The correct position. 123 | */ 124 | private bisectRight(id: Ident): number { 125 | let min = 0; 126 | let max = this.atoms.length; 127 | 128 | while (min < max) { 129 | let curr = Math.floor((min + max) / 2); 130 | if (id.compare(this.atoms[curr].id) < 0) { 131 | max = curr; 132 | } 133 | else { 134 | min = curr + 1; 135 | } 136 | } 137 | 138 | return min; 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/idents/Ident.ts: -------------------------------------------------------------------------------- 1 | import {Segment} from './Segment'; 2 | 3 | /** 4 | * An identifier that can uniquely identify an atom in a sequence. 5 | */ 6 | export class Ident { 7 | 8 | /** 9 | * The local logical time of the replica that created the identifier. 10 | */ 11 | time: number 12 | 13 | /** 14 | * The ordered set of path segments that make up the identifier. 15 | */ 16 | private path: Segment[] 17 | 18 | /** 19 | * Creates an instance of Ident. 20 | * @param time The local logical time of the creating replica. 21 | * @param path The ordered set of path segments. 22 | * @returns An instance of Ident. 23 | */ 24 | constructor(time: number, path: Segment[]) { 25 | this.time = time; 26 | this.path = path; 27 | } 28 | 29 | /** 30 | * Converts a string representation into an Ident. 31 | * @param str The string to parse. 32 | * @returns The parsed instance of Ident. 33 | */ 34 | static parse(str: string): Ident { 35 | try { 36 | let [time, pathstr] = str.split('#'); 37 | if (time === undefined || time.length == 0) { 38 | throw new Error("The ident is missing a timestamp"); 39 | } 40 | if (pathstr === undefined || pathstr.length == 0) { 41 | throw new Error("The ident is missing a path"); 42 | } 43 | let prev = undefined; 44 | let path = pathstr.split('.').map((token) => { 45 | let [digit, replica] = token.split(':', 2); 46 | if (!replica) replica = prev; 47 | else prev = replica; 48 | return Segment(Number(digit), replica); 49 | }); 50 | return new Ident(Number(time), path); 51 | } 52 | catch (err) { 53 | throw new Error(`Error parsing Ident: ${err}`); 54 | } 55 | } 56 | 57 | /** 58 | * Gets the Segment of the identifier at the specified depth. 59 | * @param depth The desired depth. 60 | * @returns The Segment at the depth. 61 | */ 62 | get(depth: number): Segment { 63 | return this.path[depth]; 64 | } 65 | 66 | /** 67 | * Gets the depth of the path (the number of segments it contains). 68 | * @returns The depth. 69 | */ 70 | depth(): number { 71 | return this.path.length; 72 | } 73 | 74 | /** 75 | * Compares the value of this identifier to another. 76 | * @param other The identifier to compare. 77 | * @returns -1 if this identifier is lesser, 78 | * 1 if it is greater, 79 | * or 0 if they are equal. 80 | */ 81 | compare(other: Ident): number { 82 | let depth = Math.max(this.path.length, other.path.length); 83 | for (let i = 0; i < depth; i++) { 84 | let my = this.get(i); 85 | let their = other.get(i); 86 | if (my === undefined && their !== undefined) return -1; 87 | if (my !== undefined && their === undefined) return 1; 88 | if (my.digit < their.digit) return -1; 89 | if (my.digit > their.digit) return 1; 90 | if (my.replica < their.replica) return -1; 91 | if (my.replica > their.replica) return 1; 92 | } 93 | if (this.time < other.time) return -1; 94 | if (this.time > other.time) return 1; 95 | return 0; 96 | } 97 | 98 | /** 99 | * Encodes the identifier as a compact string representation. 100 | * @returns The string representation. 101 | */ 102 | toString(): string { 103 | let prev = undefined; 104 | let path = this.path.map((seg) => { 105 | if (seg.replica == prev) { 106 | return seg.digit; 107 | } 108 | else { 109 | prev = seg.replica; 110 | return `${seg.digit}:${seg.replica}`; 111 | } 112 | }); 113 | return `${this.time}#${path.join('.')}`; 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /src/idents/LSEQIdentGenerator.ts: -------------------------------------------------------------------------------- 1 | import {Ident} from './Ident'; 2 | import {IdentGenerator} from './IdentGenerator'; 3 | import {Segment} from './Segment'; 4 | 5 | /** 6 | * The identifier allocation strategy to use at a specified depth. 7 | */ 8 | enum LSEQStrategy { 9 | 10 | /** 11 | * Generate identifiers by adding a value to the previous digit. 12 | */ 13 | AddFromLeft = 1, 14 | 15 | /** 16 | * Generate identifiers by subtracting a value to the next digit. 17 | */ 18 | SubtractFromRight = 2, 19 | 20 | } 21 | 22 | /** 23 | * An IdentGenerator that implements LSEQ to create identifiers. 24 | */ 25 | export class LSEQIdentGenerator implements IdentGenerator { 26 | 27 | startingWidth: number 28 | maxDistance: number 29 | strategies: LSEQStrategy[] 30 | first: Ident 31 | last: Ident 32 | 33 | /** 34 | * Creates an instance of LSEQIdentGenerator. 35 | * @param startingWidth The width (2^x) of the first level of Idents. 36 | * @param maxDistance The maximum delta between two Idents. 37 | * @returns An instance of LSEQIdentGenerator. 38 | */ 39 | constructor(startingWidth: number = 4, maxDistance: number = 10) { 40 | this.startingWidth = startingWidth; 41 | this.maxDistance = maxDistance; 42 | this.strategies = []; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | getIdent(name: string, time: number, before: Ident, after: Ident): Ident { 49 | 50 | if (!before) before = this.getFirst(name); 51 | if (!after) after = this.getLast(name); 52 | 53 | let distance: number = 0; 54 | let depth: number = -1; 55 | let min: number = 0; 56 | let max: number = 0; 57 | 58 | while (distance < 1) { 59 | depth++; 60 | let left = before.get(depth); 61 | let right = after.get(depth); 62 | min = left ? left.digit : 0; 63 | max = right ? right.digit : this.getWidthAtDepth(depth); 64 | distance = max - min - 1; 65 | } 66 | 67 | let boundary = Math.min(distance, this.maxDistance); 68 | let delta = Math.floor(Math.random() * boundary) + 1; 69 | let strategy = this.getStrategyAtDepth(depth); 70 | 71 | let path = []; 72 | for (let i = 0; i < depth; i++) { 73 | path.push(before.get(i) || Segment(0, name)); 74 | } 75 | 76 | if (strategy == LSEQStrategy.AddFromLeft) { 77 | path.push(Segment(min + delta, name)); 78 | } 79 | else { 80 | path.push(Segment(max - delta, name)); 81 | } 82 | 83 | return new Ident(time, path); 84 | } 85 | 86 | /** 87 | * Gets the maximum addressable digit at the specified depth. This is 88 | * generally 2^(depth + startingWidth) - 1, with a maximum of 2^53 - 1 89 | * (the largest integer that can be stored in a Number.) 90 | * @param depth The desired depth. 91 | * @returns The maximum addressable digit at the specified depth. 92 | */ 93 | private getWidthAtDepth(depth: number): number { 94 | let power = depth + this.startingWidth; 95 | if (power > 53) power = 53; 96 | return 2**power - 1; 97 | } 98 | 99 | /** 100 | * Gets the digit allocation strategy for the specified depth. 101 | * If none has been selected, one is chosen at random. 102 | * @param depth The desired depth. 103 | * @returns The strategy to use at that depth. 104 | */ 105 | private getStrategyAtDepth(depth: number): LSEQStrategy { 106 | let strategy = this.strategies[depth]; 107 | if (!strategy) { 108 | let random = Math.floor(Math.random() * 2) + 1; 109 | strategy = this.strategies[depth] = random; 110 | } 111 | return strategy; 112 | } 113 | 114 | /** 115 | * Gets the first possible ident that can be generated. 116 | * @param name The replica name. 117 | * @returns The ident. 118 | */ 119 | private getFirst(name: string): Ident { 120 | if (!this.first) this.first = new Ident(0, [Segment(0, name)]); 121 | return this.first; 122 | } 123 | 124 | /** 125 | * Gets the first possible ident that can be generated. 126 | * @param name The replica name. 127 | * @returns The ident. 128 | */ 129 | private getLast(name: string): Ident { 130 | if (!this.last) this.last = new Ident(0, [Segment(this.getWidthAtDepth(0), name)]); 131 | return this.last; 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /src/Op.ts: -------------------------------------------------------------------------------- 1 | import {Ident} from './idents'; 2 | 3 | /** 4 | * The kind of operation. 5 | */ 6 | export enum OpKind { 7 | 8 | /** 9 | * The insertion of a value. 10 | */ 11 | Insert = 1, 12 | 13 | /** 14 | * The removal of a value. 15 | */ 16 | Remove = 2, 17 | } 18 | 19 | /** 20 | * An operation that can be applied to a KSeq. 21 | */ 22 | export abstract class Op { 23 | 24 | /** 25 | * The kind of operation. 26 | */ 27 | kind: OpKind 28 | 29 | /** 30 | * The name of the replica on which the operation was performed. 31 | */ 32 | replica: string 33 | 34 | /** 35 | * A UNIX epoch timestamp for the wall time at which the operation was performed. 36 | * (Note: since this is generated by the local replica's clock, it is just an 37 | * approximation and should not be used to create a total order.) 38 | */ 39 | timestamp: number 40 | 41 | /** 42 | * Creates an instance of Op. 43 | * @param kind The kind of operation. 44 | * @param replica The name of the replica on which the operation was performed. 45 | * @param timestamp A UNIX epoch timestamp for the wall time when the operation was performed. 46 | * @returns An instance of Op. 47 | */ 48 | constructor(kind: OpKind, replica: string, timestamp: number) { 49 | this.kind = kind; 50 | this.replica = replica; 51 | this.timestamp = timestamp; 52 | } 53 | 54 | /** 55 | * Converts an encoded string to an Op of the correct type. 56 | * @param str The encoded string. 57 | * @returns An instance of the encoded Op. 58 | */ 59 | static parse(str: string): Op { 60 | const kind = str[0]; 61 | switch(kind) { 62 | case '+': return InsertOp.parse(str); 63 | case '-': return RemoveOp.parse(str); 64 | } 65 | } 66 | 67 | /** 68 | * Encodes the Op as a compact string representation. 69 | */ 70 | abstract toString(): string; 71 | 72 | } 73 | 74 | /** 75 | * An operation that inserts an atom into the sequence with the specified 76 | * identifier and value. 77 | */ 78 | export class InsertOp extends Op { 79 | 80 | /** 81 | * The identifier for the value. 82 | */ 83 | id: Ident 84 | 85 | /** 86 | * The value to insert. 87 | */ 88 | value: any 89 | 90 | /** 91 | * Creates an instance of InsertOp. 92 | * @param replica The name of the replica on which the operation was performed. 93 | * @param timestamp A UNIX epoch timestamp for the wall time when the operation was performed. 94 | * @param id The identifier for the value. 95 | * @param value The value to insert. 96 | * @returns An instance of InsertOp. 97 | */ 98 | constructor(replica: string, timestamp: number, id: Ident, value: any) { 99 | super(OpKind.Insert, replica, timestamp) 100 | this.id = id; 101 | this.value = value; 102 | } 103 | 104 | /** 105 | * Converts an encoded string to an InsertOp. 106 | * @param str The encoded string. 107 | * @returns An instance of the encoded InsertOp. 108 | */ 109 | static parse(str: string): InsertOp { 110 | let [timestamp, replica, id, value] = str.substr(1).split('/', 4); 111 | return new InsertOp(replica, Number(timestamp), Ident.parse(str), JSON.parse(value)); 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | toString() { 118 | return `+${this.timestamp}/${this.replica}/${this.id.toString()}/${JSON.stringify(this.value)}`; 119 | } 120 | 121 | } 122 | 123 | /** 124 | * An operation that removes an atom with the specified identifer. 125 | */ 126 | export class RemoveOp extends Op { 127 | 128 | /** 129 | * The identifier to remove. 130 | */ 131 | id: Ident 132 | 133 | /** 134 | * Creates an instance of RemoveOp. 135 | * @param replica The name of the replica on which the operation was performed. 136 | * @param timestamp A UNIX epoch timestamp for the wall time when the operation was performed. 137 | * @param id The identifier of the atom to remove. 138 | * @returns An instance of RemoveOp. 139 | */ 140 | constructor(replica: string, timestamp: number, id: Ident) { 141 | super(OpKind.Remove, replica, timestamp) 142 | this.id = id; 143 | } 144 | 145 | /** 146 | * Converts an encoded string to an RemoveOp. 147 | * @param str The encoded string. 148 | * @returns An instance of the encoded RemoveOp. 149 | */ 150 | static parse(str: string): RemoveOp { 151 | let [timestamp, replica, id] = str.substr(1).split('/', 3); 152 | return new RemoveOp(replica, Number(timestamp), Ident.parse(str)); 153 | } 154 | 155 | /** 156 | * @inheritdoc 157 | */ 158 | toString() { 159 | return `-${this.timestamp}/${this.replica}/${this.id.toString()}`; 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KSeq 2 | 3 | KSeq is an implementation of a simple CRDT (conflict-free replicated data type) that represents 4 | an ordered sequence of items. Designed for use in collaborative editors, it allows various 5 | contributors to concurrently alter the sequence, while preserving both data and intent. 6 | 7 | **Note**: This is still a work in progress, and has not yet been deployed in any real-world system. 8 | 9 | # Overview 10 | 11 | CRDTs are data types on which the same set of operations yields the same state, regardless 12 | of whether the operations are duplicated or received out of order. This means that the 13 | state of multiple replicas will (eventually) converge without the need for consensus between 14 | the replicas. To achieve this, all operations of a CRDT must be: 15 | 16 | - *Associative*: `A + (B + C) = (A + B) + C`, that is, grouping of operations doesn't matter. 17 | - *Commutative*: `A + B = B + A`, that is, the order of execution doesn't matter. 18 | - *Idempotent*: `A + A = A`, that is, the duplication of operations doesn't matter. 19 | 20 | CRDTs are related to [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation) (OT), 21 | which was popularized by Apache (then Google) Wave in 2009. However, OT requires comparatively 22 | complex semantics to transform operations to ensure their commutativity. 23 | 24 | Further, for a system to be usable in a collaborative editing system, it must satisfy the 25 | CCI criteria [1]: 26 | 27 | - *Causality*: Operations must be executed in the same order on every replica. 28 | - *Convergence*: The state of every replica becomes identical when the system is at rest. 29 | - *Intention*: The expected effect of an operation must be observed on all replicas. 30 | 31 | KSeq satisfies the requirements of a CRDT, as well as the CCI criteria. (Actually, while KSeq 32 | does *not* guarantee the causal execution of operations, it does guarantee that the state 33 | will be altered in a way such that it *appears* that the operations were executed in causal order.) 34 | 35 | # Approach 36 | 37 | KSeq is an operations-based, continuous sequence CRDT [2] based on the Logoot [3] 38 | and LSEQ [4] systems. However, these structures make the assumption of causal delivery of messages, 39 | either limiting their application or requiring an external system to enforce this guarantee. 40 | 41 | KSeq combines the *identifier tree* approach used by LSEQ/Logoot with an additional G-Set 42 | to track removal of items, thus eliminating the need for causal delivery at the cost of 43 | storage overhead. (This technique is similar to that used in 2P-Set, but KSeq also provides 44 | a guarantee that the items inserted will be ordered.) 45 | 46 | Therefore, a KSeq is a façade over two internal sets: 47 | 48 | 1. An ordered set **S** of *atoms* representing the values currently contained in the KSeq. 49 | Each atom is a `[id, value]` tuple, and the set is ordered by the ids. 50 | 2. An unordered set **R**, which contains the ids of all atoms that have been removed 51 | from the KSeq. 52 | 53 | KSeq supports two basic operations, *insert*, and *remove*. The application of these operations is simple: 54 | 55 | ``` 56 | function insert(id, value) { 57 | if !R.has(id) then S.add([id, value]) 58 | } 59 | 60 | function remove(id) { 61 | S.remove(id) 62 | R.add(id) 63 | } 64 | ``` 65 | 66 | Rather than require consumers to interact with the generation of ids, KSeq exposes a position-based 67 | API similar to a regular array, while adding convenience functions that implement special-cases of 68 | the *insert* and *remove* operations. 69 | 70 | For example: 71 | 72 | ```ts 73 | let list = new KSeq(); 74 | list.append('a'); 75 | list.insert('b', 0); 76 | list.append('c'); 77 | list.remove(1); 78 | assert(list.size() == 2); 79 | assert(list.get(0) == 'b'); 80 | assert(list.get(1) == 'c'); 81 | ``` 82 | 83 | However, it also returns operations that may be applied to other KSeqs to achieve convergent state: 84 | 85 | ```ts 86 | let alice = new KSeq('alice'); 87 | let bob = new KSeq('bob'); 88 | let op1 = alice.append('h'); 89 | let op2 = alice.append('i'); 90 | // Note that these operations may be applied out of order. 91 | bob.apply(op2); 92 | bob.apply(op1); 93 | assert(bob.get(0) == 'h'); 94 | assert(bob.get(1) == 'i'); 95 | ``` 96 | 97 | These operations would generally be serialized and passed over a network to a remote machine that housed 98 | its own replica. 99 | 100 | # Limitations 101 | 102 | While KSeq is tolerant to message loss and incorrect delivery, in order for the state of all replicas 103 | to converge, all operations *must eventually be applied* to all replicas. This is a limitation of 104 | operations-based CRDTs, and is probably best solved through read-repair. 105 | 106 | Currently, KSeq is implemented with a rudimentary id generation system using simple JS `Number`s. 107 | This is memory-intensive, and may benefit from an dense bitfield implementation as described in [4]. 108 | However, further testing is needed, since the cost of conversion between strings and bitfields 109 | may outweigh the improved memory usage. 110 | 111 | As a KSeq is edited, newly-generated identifiers will become longer, and the cardinality of **R** will 112 | continue to grow. This means that the performance of the KSeq will gradually degrade over time. This 113 | could be solved by a garbage collection routine that would remove very old items from **R**, but this 114 | would require at best a heuristic (eg. remove all items older than some wall time) or at worst, 115 | a consensus-based algorithm. 116 | 117 | # Further Reading 118 | 119 | 1. [Achieving Convergence, Causality-Preservation, and Intention-Preservation in Real-Time Cooperative Editing Systems](http://diyhpl.us/~bryan/papers2/distributed/distributed-systems/real-time-cooperative-editing-systems.1998.pdf), Sun et. al. 1998 120 | 2. [A Comprehensive Study of Convergent and Commutative Replicated Data Types](http://hal.upmc.fr/inria-00555588/document), Shapiro et. al. 2011 121 | 3. [Logoot: A Scalable Optimistic Replication Algorithm for Collaborative Editing on P2P Networks](https://hal.archives-ouvertes.fr/inria-00432368/document), Weiss et. al. 2009 122 | 4. [LSEQ: an Adaptive Structure for Sequences in Distributed Collaborative Editing](http://hal.univ-nantes.fr/file/index/docid/921633/filename/fp025-nedelec.pdf), Nédelec, et. al. 2013 123 | -------------------------------------------------------------------------------- /src/KSeq.ts: -------------------------------------------------------------------------------- 1 | import {Ident, IdentSet, IdentGenerator, LSEQIdentGenerator, Segment} from './idents'; 2 | import {AtomList, ArrayAtomList} from './storage'; 3 | import {Op, OpKind, InsertOp, RemoveOp} from './Op'; 4 | 5 | /** 6 | * A CmRDT sequence that supports concurrent simultaneous editing 7 | * while preserving the intention of each edit. 8 | */ 9 | export class KSeq { 10 | 11 | /** 12 | * The unique name of this replica. 13 | */ 14 | name: string 15 | 16 | /** 17 | * The current logical time. 18 | */ 19 | private time: number 20 | 21 | /** 22 | * The ordered list of atoms in the sequence. 23 | */ 24 | private atoms: AtomList 25 | 26 | /** 27 | * The set of idents of atoms that have been removed. 28 | */ 29 | private removed: IdentSet 30 | 31 | /** 32 | * The generator used to create unique idents for new atoms. 33 | */ 34 | private identGenerator: IdentGenerator 35 | 36 | /** 37 | * Creates an instance of KSeq. 38 | * @param name The unique name for this replica. 39 | * @param atoms The backing storage, if null, creates a new ArrayAtomList. 40 | * @param identGenerator The id generator, if null, creates a new LSEQIdentGenerator. 41 | * @returns An instance of KSeq. 42 | */ 43 | constructor(name: string, atoms?: AtomList, identGenerator?: IdentGenerator) { 44 | this.name = name; 45 | this.time = 0; 46 | this.atoms = atoms || new ArrayAtomList(); 47 | this.removed = new IdentSet(); 48 | this.identGenerator = identGenerator || new LSEQIdentGenerator(); 49 | } 50 | 51 | /** 52 | * Gets the number of items in the sequence. 53 | * @returns The number of items in the sequence. 54 | */ 55 | size(): number { 56 | return this.atoms.size(); 57 | } 58 | 59 | /** 60 | * Gets the maximum depth of identifiers in the sequence. 61 | * @returns The depth of the sequence. 62 | */ 63 | depth(): number { 64 | let max = 0; 65 | this.forEach((atom) => { 66 | let depth = atom.id.depth(); 67 | if (max < depth) max = depth; 68 | }); 69 | return max; 70 | } 71 | 72 | /** 73 | * Inserts a value into the sequence at the specified position. 74 | * @param value The value to insert. 75 | * @param pos The position at which to insert the value. 76 | * @returns An InsertOp that can be applied to other KSeqs 77 | * to reproduce the insertion. 78 | */ 79 | insert(value: T, pos: number): InsertOp { 80 | if (pos < 0) throw new RangeError(`The position ${pos} must be greater than or equal to zero.`); 81 | 82 | let before = this.atoms.get(pos - 1); 83 | let after = this.atoms.get(pos); 84 | let id = this.identGenerator.getIdent(this.name, ++this.time, (before && before.id), (after && after.id)); 85 | let op = new InsertOp(this.name, this.getWallTime(), id, value); 86 | this.apply(op); 87 | 88 | return op; 89 | } 90 | 91 | /** 92 | * Appends a value to the end of the sequence. 93 | * @param value The value to append. 94 | * @returns An InsertOp that can be applied to other KSeqs 95 | * to reproduce the insertion. 96 | */ 97 | append(value: T): InsertOp { 98 | return this.insert(value, this.size()); 99 | } 100 | 101 | /** 102 | * Removes the value at the specified position from the sequence. 103 | * @param pos The position of the value to remove. 104 | * @returns An RemoveOp that can be applied to other KSeqs 105 | * to reproduce the removal. 106 | */ 107 | remove(pos: number): RemoveOp { 108 | if (pos < 0) throw new RangeError(`The position ${pos} must be greater than or equal to zero.`); 109 | 110 | let atom = this.atoms.get(pos); 111 | if (atom) { 112 | let op = new RemoveOp(this.name, this.getWallTime(), atom.id) 113 | this.apply(op); 114 | return op; 115 | } 116 | 117 | return null; 118 | } 119 | 120 | /** 121 | * Gets the value at the specified position in the sequence. 122 | * @param pos The desired position. 123 | * @returns The value at that position, 124 | * or undefined if no such value exists. 125 | */ 126 | get(pos: number): T { 127 | const atom = this.atoms.get(pos); 128 | return atom ? atom.value : undefined; 129 | } 130 | 131 | /** 132 | * Applies a function to each of the values in the sequence. 133 | * @param func The function to apply. 134 | */ 135 | forEach(func: { (T): void }): void { 136 | this.atoms.forEach((atom) => func(atom.value)); 137 | } 138 | 139 | /** 140 | * Applies a transformation function to each of the values in the sequence. 141 | * @param func The transformation function to apply. 142 | * @returns An array containing the results of the function calls. 143 | */ 144 | map(func: { (T): R }): R[] { 145 | return this.atoms.map((atom) => func(atom.value)); 146 | } 147 | 148 | /** 149 | * Converts the sequence to an array. 150 | * @returns An array representation of the values in the sequence. 151 | */ 152 | toArray(): T[] { 153 | return this.atoms.map((atom) => atom.value); 154 | } 155 | 156 | /** 157 | * Converts the sequence to a compact object suitable for serialization. 158 | * @returns A serializable object. 159 | */ 160 | toJSON(): Object { 161 | return { 162 | n: this.name, 163 | t: this.time, 164 | s: this.atoms.map((atom) => [atom.id.toString(), atom.value]), 165 | r: this.removed.toJSON() 166 | } 167 | } 168 | 169 | /** 170 | * Applies the specified Op to the sequence. This can be used to apply 171 | * operations that have been generated by remote sequences. 172 | * @param op The Op to apply. 173 | */ 174 | apply(op: Op): void { 175 | switch (op.kind) { 176 | case OpKind.Insert: 177 | let insertOp = op; 178 | // If an atom with the specified ident has already been removed, 179 | // the ops have been received out of order. We should ignore the insert. 180 | if (!this.removed.has(insertOp.id)) { 181 | this.atoms.add(insertOp.id, insertOp.value); 182 | } 183 | break; 184 | case OpKind.Remove: 185 | let removeOp = op; 186 | // Ignore repeated remove ops. 187 | if (!this.removed.has(removeOp.id)) { 188 | this.atoms.remove(removeOp.id); 189 | this.removed.add(removeOp.id); 190 | } 191 | break; 192 | default: 193 | throw new Error(`Unknown op kind ${op.kind}`); 194 | } 195 | } 196 | 197 | /** 198 | * Gets the current wall time as a UNIX epoch timestamp. 199 | * @returns An integer representing the wall time. 200 | */ 201 | private getWallTime(): number { 202 | return Math.floor(new Date().valueOf() / 1000); 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /test/ArrayAtomList.tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import {ArrayAtomList} from '../src/storage'; 5 | import {Ident, Segment} from '../src/idents'; 6 | import {assert} from 'chai'; 7 | 8 | describe("ArrayAtomList", () => { 9 | 10 | //-------------------------------------------------------------------------------- 11 | 12 | describe("constructor()", () => { 13 | 14 | it("returns an ArrayAtomList instance", () => { 15 | let list = new ArrayAtomList(); 16 | assert.instanceOf(list, ArrayAtomList); 17 | }); 18 | 19 | }); 20 | 21 | //-------------------------------------------------------------------------------- 22 | 23 | describe("indexOf()", () => { 24 | 25 | it("compares idents by value, not identity", () => { 26 | let list = new ArrayAtomList(); 27 | list.add(Ident.parse("1#0:foo"), 'line one'); 28 | let ident = Ident.parse("1#0:foo"); 29 | assert.equal(list.indexOf(ident), 0); 30 | }); 31 | 32 | describe("when the ident doesn't exist in the list", () => { 33 | 34 | it("returns -1", () => { 35 | let list = new ArrayAtomList(); 36 | let x = Ident.parse("1#0:foo"); 37 | let y = Ident.parse("1#1:foo"); 38 | list.add(x, 'line one'); 39 | assert.equal(list.indexOf(y), -1); 40 | }); 41 | 42 | }); 43 | 44 | describe("when the ident exists in the list", () => { 45 | 46 | it("returns the position of the atom with the ident", () => { 47 | let list = new ArrayAtomList(); 48 | let x = Ident.parse("1#0:foo"); 49 | let y = Ident.parse("1#0:bar"); 50 | let z = Ident.parse("1#0:zap"); 51 | list.add(x, 'line one'); 52 | list.add(y, 'line two'); 53 | list.add(z, 'line three'); 54 | assert.equal(list.indexOf(x), 1); 55 | }); 56 | 57 | }); 58 | 59 | }); 60 | 61 | //-------------------------------------------------------------------------------- 62 | 63 | describe("add()", () => { 64 | 65 | describe("when the ident doesn't already exist in the list", () => { 66 | 67 | it("adds a new atom", () => { 68 | let list = new ArrayAtomList(); 69 | let ident = Ident.parse("1#0:foo"); 70 | let value = 'line one' 71 | list.add(ident, value); 72 | let atom = list.get(0); 73 | assert.equal(atom.id.compare(ident), 0); 74 | assert.equal(atom.value, value); 75 | }); 76 | 77 | it("returns the insert position", () => { 78 | let list = new ArrayAtomList(); 79 | let ident = Ident.parse("1#0:foo"); 80 | let ret = list.add(ident, 'line one'); 81 | assert.equal(ret, 0); 82 | }); 83 | 84 | }); 85 | 86 | describe("when the ident already exists in the list", () => { 87 | 88 | it("does not add an atom", () => { 89 | let list = new ArrayAtomList(); 90 | let x = Ident.parse("1#0:foo"); 91 | let y = Ident.parse("1#0:foo"); 92 | list.add(x, 'line one'); 93 | list.add(y, 'line two'); 94 | assert.equal(list.size(), 1); 95 | assert.equal(list.get(0).value, 'line one'); 96 | }); 97 | 98 | it("returns -1", () => { 99 | let list = new ArrayAtomList(); 100 | let x = Ident.parse("1#0:foo"); 101 | let y = Ident.parse("1#0:foo"); 102 | list.add(x, 'line one'); 103 | let ret = list.add(y, 'line two'); 104 | assert.equal(ret, -1); 105 | }); 106 | 107 | }); 108 | 109 | }); 110 | 111 | //-------------------------------------------------------------------------------- 112 | 113 | describe("remove()", () => { 114 | 115 | describe("when the ident exists in the list", () => { 116 | 117 | it("removes the atom with the ident", () => { 118 | let list = new ArrayAtomList(); 119 | let ident = Ident.parse("1#0:foo"); 120 | list.add(ident, 'line one'); 121 | assert.equal(list.size(), 1); 122 | list.remove(ident); 123 | assert.equal(list.size(), 0); 124 | }); 125 | 126 | it("returns the position that the atom occupied", () => { 127 | let list = new ArrayAtomList(); 128 | let ident = Ident.parse("1#0:foo"); 129 | list.add(ident, 'line one'); 130 | let pos = list.remove(ident); 131 | assert.equal(pos, 0); 132 | }); 133 | 134 | }); 135 | 136 | describe("when the ident doesn't exist in the list", () => { 137 | 138 | it("returns -1", () => { 139 | let list = new ArrayAtomList(); 140 | let ident = Ident.parse("1#0:foo"); 141 | let pos = list.remove(ident); 142 | assert.equal(pos, -1); 143 | }); 144 | 145 | }); 146 | 147 | }); 148 | 149 | //-------------------------------------------------------------------------------- 150 | 151 | describe("forEach()", () => { 152 | 153 | it("applies the function to each atom in the list", () => { 154 | let list = new ArrayAtomList(); 155 | let x = Ident.parse("1#0:foo"); 156 | let y = Ident.parse("1#1:foo"); 157 | list.add(x, 'line one'); 158 | list.add(y, 'line two'); 159 | let calls = []; 160 | list.forEach((atom) => calls.push(atom.value)); 161 | assert.deepEqual(calls, ['line one', 'line two']); 162 | }); 163 | 164 | }); 165 | 166 | //-------------------------------------------------------------------------------- 167 | 168 | describe("map()", () => { 169 | 170 | it("applies the function to each atom in the list", () => { 171 | let list = new ArrayAtomList(); 172 | let x = Ident.parse("1#0:foo"); 173 | let y = Ident.parse("1#1:foo"); 174 | list.add(x, 'line one'); 175 | list.add(y, 'line two'); 176 | let values = list.map((atom) => atom.value); 177 | assert.deepEqual(values, ['line one', 'line two']); 178 | }); 179 | 180 | }); 181 | 182 | //-------------------------------------------------------------------------------- 183 | 184 | describe("toArray()", () => { 185 | 186 | it("returns an array of atoms in the list", () => { 187 | let list = new ArrayAtomList(); 188 | let x = Ident.parse("1#0:foo"); 189 | let y = Ident.parse("1#1:foo"); 190 | list.add(x, 'line one'); 191 | list.add(y, 'line two'); 192 | let atoms = list.toArray(); 193 | let values = atoms.map((atom) => atom.value); 194 | assert.deepEqual(values, ['line one', 'line two']); 195 | }); 196 | 197 | }); 198 | 199 | //-------------------------------------------------------------------------------- 200 | 201 | }); -------------------------------------------------------------------------------- /test/IdentSet.tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import {IdentSet, Ident} from '../src/idents'; 5 | import {assert} from 'chai'; 6 | 7 | describe("IdentSet", () => { 8 | 9 | //-------------------------------------------------------------------------------- 10 | 11 | describe("constructor()", () => { 12 | 13 | describe("with no iterable", () => { 14 | 15 | it("returns an empty IdentSet", () => { 16 | let set = new IdentSet(); 17 | assert.instanceOf(set, IdentSet); 18 | assert.equal(set.size(), 0); 19 | }); 20 | 21 | }); 22 | 23 | describe("with an iterable of Idents", () => { 24 | 25 | it("returns an IdentSet populated with the idents", () => { 26 | let set = new IdentSet([Ident.parse("1#0:foo"), Ident.parse("2#1:foo")]); 27 | assert.instanceOf(set, IdentSet); 28 | assert.equal(set.size(), 2); 29 | assert.isTrue(set.has("1#0:foo")); 30 | assert.isTrue(set.has("2#1:foo")); 31 | }); 32 | 33 | }); 34 | 35 | describe("with an iterable of serialized Idents", () => { 36 | 37 | it("returns an IdentSet populated with the idents", () => { 38 | let set = new IdentSet(["1#0:foo", "2#1:foo"]); 39 | assert.instanceOf(set, IdentSet); 40 | assert.equal(set.size(), 2); 41 | assert.isTrue(set.has("1#0:foo")); 42 | assert.isTrue(set.has("2#1:foo")); 43 | }); 44 | }); 45 | 46 | }); 47 | 48 | //-------------------------------------------------------------------------------- 49 | 50 | describe("size()", () => { 51 | 52 | it("returns the number of entries in the set", () => { 53 | let set = new IdentSet(["1#0:foo", "2#1:foo", "3#2:foo"]); 54 | assert.equal(set.size(), 3); 55 | }) 56 | 57 | }); 58 | 59 | //-------------------------------------------------------------------------------- 60 | 61 | describe("add()", () => { 62 | 63 | describe("when passed an Ident", () => { 64 | 65 | describe("when the ident is not already in the set", () => { 66 | 67 | it("adds the ident to the set", () => { 68 | let set = new IdentSet(); 69 | set.add(Ident.parse("1#0:foo")); 70 | assert.equal(set.size(), 1); 71 | assert.isTrue(set.has("1#0:foo")); 72 | }); 73 | 74 | }); 75 | 76 | describe("when the ident is already in the set", () => { 77 | 78 | it("does not alter the set", () => { 79 | let set = new IdentSet(["1#0:foo"]); 80 | set.add(Ident.parse("1#0:foo")); 81 | assert.equal(set.size(), 1); 82 | }); 83 | 84 | }); 85 | 86 | }); 87 | 88 | describe("when passed a serialized ident", () => { 89 | 90 | describe("when the ident is not already in the set", () => { 91 | 92 | it("adds the ident to the set", () => { 93 | let set = new IdentSet(); 94 | set.add("1#0:foo"); 95 | assert.equal(set.size(), 1); 96 | assert.isTrue(set.has("1#0:foo")); 97 | }); 98 | 99 | }); 100 | 101 | describe("when the ident is already in the set", () => { 102 | 103 | it("does not alter the set", () => { 104 | let set = new IdentSet(["1#0:foo"]); 105 | set.add("1#0:foo"); 106 | assert.equal(set.size(), 1); 107 | }); 108 | 109 | }); 110 | 111 | }); 112 | 113 | }); 114 | 115 | //-------------------------------------------------------------------------------- 116 | 117 | describe("has()", () => { 118 | 119 | describe("when passed an Ident", () => { 120 | 121 | describe("when the ident is in the set", () => { 122 | 123 | it("returns true", () => { 124 | let set = new IdentSet(["1#0:foo"]); 125 | let ident = Ident.parse("1#0:foo"); 126 | assert.isTrue(set.has(ident)); 127 | }); 128 | 129 | }); 130 | 131 | describe("when the ident is not in the set", () => { 132 | 133 | it("returns false", () => { 134 | let set = new IdentSet(); 135 | let ident = Ident.parse("1#0:foo"); 136 | assert.isFalse(set.has(ident)); 137 | }); 138 | 139 | }); 140 | 141 | }); 142 | 143 | describe("when passed serialized ident", () => { 144 | 145 | describe("when the ident is in the set", () => { 146 | 147 | it("returns true", () => { 148 | let set = new IdentSet(["1#0:foo"]); 149 | assert.isTrue(set.has("1#0:foo")); 150 | }); 151 | 152 | }); 153 | 154 | describe("when the ident is not in the set", () => { 155 | 156 | it("returns false", () => { 157 | let set = new IdentSet(); 158 | assert.isFalse(set.has("1#0:foo")); 159 | }); 160 | 161 | }); 162 | 163 | }); 164 | 165 | }); 166 | 167 | //-------------------------------------------------------------------------------- 168 | 169 | describe("remove()", () => { 170 | 171 | describe("when passed an Ident", () => { 172 | 173 | describe("when the ident is in the set", () => { 174 | 175 | it("removes the ident", () => { 176 | let set = new IdentSet(["1#0:foo"]); 177 | let ident = Ident.parse("1#0:foo"); 178 | assert.isTrue(set.has(ident)); 179 | set.remove(ident); 180 | assert.isFalse(set.has(ident)); 181 | }); 182 | 183 | }); 184 | 185 | describe("when the ident is not in the set", () => { 186 | 187 | it("does not alter the set", () => { 188 | let set = new IdentSet(["1#0:foo"]); 189 | let ident = Ident.parse("2#1:foo"); 190 | assert.equal(set.size(), 1); 191 | assert.isFalse(set.has(ident)); 192 | set.remove(ident); 193 | assert.equal(set.size(), 1); 194 | }); 195 | 196 | }); 197 | 198 | }); 199 | 200 | describe("when passed a serialized ident", () => { 201 | 202 | describe("when the ident is in the set", () => { 203 | 204 | it("removes the ident", () => { 205 | let set = new IdentSet(["1#0:foo"]); 206 | assert.isTrue(set.has("1#0:foo")); 207 | set.remove("1#0:foo"); 208 | assert.isFalse(set.has("1#0:foo")); 209 | }); 210 | 211 | }); 212 | 213 | describe("when the ident is not in the set", () => { 214 | 215 | it("does not alter the set", () => { 216 | let set = new IdentSet(["1#0:foo"]); 217 | assert.equal(set.size(), 1); 218 | assert.isFalse(set.has("2#1:foo")); 219 | set.remove("2#1:foo"); 220 | assert.equal(set.size(), 1); 221 | }); 222 | 223 | }); 224 | 225 | }); 226 | 227 | }); 228 | 229 | //-------------------------------------------------------------------------------- 230 | 231 | }); 232 | -------------------------------------------------------------------------------- /test/Ident.tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import {Ident, Segment} from '../src/idents'; 5 | import {assert} from 'chai'; 6 | 7 | describe("Ident", () => { 8 | 9 | //-------------------------------------------------------------------------------- 10 | 11 | describe("constructor()", () => { 12 | 13 | it("returns an Ident instance", () => { 14 | let ident = new Ident(0, [Segment(0, 'foo')]); 15 | assert.instanceOf(ident, Ident); 16 | }); 17 | 18 | }); 19 | 20 | //-------------------------------------------------------------------------------- 21 | 22 | describe("parse()", () => { 23 | 24 | describe("when provided an ident with one segment", () => { 25 | 26 | it("correctly parses the time", () => { 27 | let ident = Ident.parse("0#1:foo"); 28 | assert.equal(ident.time, 0); 29 | }); 30 | 31 | it("correctly parses the path of the segment", () => { 32 | let ident = Ident.parse("0#1:foo"); 33 | assert.equal(ident.depth(), 1); 34 | let segment = ident.get(0); 35 | assert.equal(segment.digit, 1); 36 | assert.equal(segment.replica, 'foo'); 37 | }); 38 | 39 | }); 40 | 41 | describe("when provided an ident with multiple segments from the same replica", () => { 42 | 43 | it("correctly parses the path", () => { 44 | let ident = Ident.parse("0#1:foo.2.3"); 45 | assert.equal(ident.depth(), 3); 46 | let expected = [1,2,3]; 47 | for (let i = 0; i < ident.depth(); i++) { 48 | assert.equal(ident.get(i).digit, expected[i]); 49 | } 50 | }); 51 | 52 | it("correctly infers the replica name from previous segments", () => { 53 | let ident = Ident.parse("0#1:foo.2.3"); 54 | assert.equal(ident.depth(), 3); 55 | for (let i = 0; i < ident.depth(); i++) { 56 | assert.equal(ident.get(i).replica, 'foo'); 57 | } 58 | }); 59 | 60 | }); 61 | 62 | describe("when provided an ident with multiple segments from different replicas", () => { 63 | 64 | it("correctly parses the path", () => { 65 | let ident = Ident.parse("0#1:foo.2.3:bar.4"); 66 | assert.equal(ident.depth(), 4); 67 | let expected = [1,2,3,4]; 68 | for (let i = 0; i < ident.depth(); i++) { 69 | assert.equal(ident.get(i).digit, expected[i]); 70 | } 71 | }); 72 | 73 | it("correctly infers the replica name from previous segments", () => { 74 | let ident = Ident.parse("0#1:foo.2.3:bar.4"); 75 | assert.equal(ident.depth(), 4); 76 | let expected = ['foo', 'foo', 'bar', 'bar']; 77 | for (let i = 0; i < ident.depth(); i++) { 78 | assert.equal(ident.get(i).replica, expected[i]); 79 | } 80 | }); 81 | 82 | }); 83 | 84 | describe("when provided an invalid ident without a time", () => { 85 | 86 | it("throws an Error", () => { 87 | let func = () => { 88 | Ident.parse("derp"); 89 | }; 90 | assert.throws(func, Error); 91 | }); 92 | 93 | }); 94 | 95 | describe("when provided an invalid ident without a path", () => { 96 | 97 | it("throws an Error", () => { 98 | let func = () => { 99 | Ident.parse("0"); 100 | }; 101 | assert.throws(func, Error); 102 | }); 103 | 104 | }); 105 | 106 | }); 107 | 108 | //-------------------------------------------------------------------------------- 109 | 110 | describe("getDepth()", () => { 111 | 112 | it("returns the correct depth", () => { 113 | let ident = Ident.parse("1#0:foo.1.2"); 114 | assert.equal(ident.depth(), 3); 115 | }); 116 | 117 | }); 118 | 119 | //-------------------------------------------------------------------------------- 120 | 121 | describe("getSegment()", () => { 122 | 123 | it("returns the path segment at the specified depth", () => { 124 | let ident = Ident.parse("1#0:foo.1.2"); 125 | assert.deepEqual(ident.get(1), Segment(1, 'foo')); 126 | }); 127 | 128 | it("if the specified depth is out of bounds, returns undefined", () => { 129 | let ident = Ident.parse("1#0:foo.1.2"); 130 | assert.equal(ident.get(12), undefined); 131 | }); 132 | 133 | }); 134 | 135 | //-------------------------------------------------------------------------------- 136 | 137 | describe("compare()", () => { 138 | 139 | it("when x is compared to itself, returns 0", () => { 140 | let x = Ident.parse("1#0:foo"); 141 | assert.equal(x.compare(x), 0); 142 | }); 143 | 144 | it("when x is identical to y, returns 0", () => { 145 | let x = Ident.parse("1#0:foo.1.2"); 146 | let y = Ident.parse("1#0:foo.1.2"); 147 | assert.equal(x.compare(y), 0); 148 | }); 149 | 150 | it("when x and y are same depth, and x[0].digit < y[0].digit, returns -1", () => { 151 | let x = Ident.parse("1#1:foo"); 152 | let y = Ident.parse("1#2:foo"); 153 | assert.equal(x.compare(y), -1); 154 | }); 155 | 156 | it("when x and y are same depth, and x[1].digit < y[1].digit, returns -1", () => { 157 | let x = Ident.parse("1#0:foo.1.2"); 158 | let y = Ident.parse("1#0:foo.2.2"); 159 | assert.equal(x.compare(y), -1); 160 | }); 161 | 162 | it("when x and y are same depth, and x[0].digit > y[0].digit, returns 1", () => { 163 | let x = Ident.parse("1#2:foo"); 164 | let y = Ident.parse("1#1:foo"); 165 | assert.equal(x.compare(y), 1); 166 | }); 167 | 168 | it("when x and y are same depth, and x[1].digit > y[1].digit, returns 1", () => { 169 | let x = Ident.parse("1#0:foo.2.2"); 170 | let y = Ident.parse("1#0:foo.1.2"); 171 | assert.equal(x.compare(y), 1); 172 | }); 173 | 174 | it("when x = [1,3] and y = [1,2,3], returns 1", () => { 175 | let x = Ident.parse("1#1:foo.3"); 176 | let y = Ident.parse("1#1:foo.2.3"); 177 | assert.equal(x.compare(y), 1); 178 | }); 179 | 180 | it("when x = [1,2,3] and y = [1,3], returns -1", () => { 181 | let x = Ident.parse("1#1:foo.2.3"); 182 | let y = Ident.parse("1#1:foo.3"); 183 | assert.equal(x.compare(y), -1); 184 | }); 185 | 186 | it("when x[0].time < y[0].time, returns -1", () => { 187 | let x = Ident.parse("1#1:foo"); 188 | let y = Ident.parse("2#1:foo"); 189 | assert.equal(x.compare(y), -1); 190 | }); 191 | 192 | it("when x[0].time > y[0].time, returns 1", () => { 193 | let x = Ident.parse("2#1:foo"); 194 | let y = Ident.parse("1#1:foo"); 195 | assert.equal(x.compare(y), 1); 196 | }); 197 | 198 | it("when x[0].site < y[0].site (lexicographically), returns -1", () => { 199 | let x = Ident.parse("1#1:bar"); 200 | let y = Ident.parse("1#1:foo"); 201 | assert.equal(x.compare(y), -1); 202 | }); 203 | 204 | it("when x[0].site > y[0].site (lexicographically), returns 1", () => { 205 | let x = Ident.parse("1#1:foo"); 206 | let y = Ident.parse("1#1:bar"); 207 | assert.equal(x.compare(y), 1); 208 | }); 209 | 210 | it("passes logoot example", () => { 211 | let x = Ident.parse("1#1:a"); 212 | let y = Ident.parse("2#1:b"); 213 | let z = Ident.parse("3#1:a.1:c"); 214 | assert.equal(x.compare(y), -1); 215 | assert.equal(y.compare(z), 1); 216 | }); 217 | 218 | }); 219 | 220 | //-------------------------------------------------------------------------------- 221 | 222 | }); -------------------------------------------------------------------------------- /typings/main/ambient/mocha/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/mocha/mocha.d.ts 3 | // Type definitions for mocha 2.2.5 4 | // Project: http://mochajs.org/ 5 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 , Vadim Macagon 6 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 7 | 8 | interface MochaSetupOptions { 9 | //milliseconds to wait before considering a test slow 10 | slow?: number; 11 | 12 | // timeout in milliseconds 13 | timeout?: number; 14 | 15 | // ui name "bdd", "tdd", "exports" etc 16 | ui?: string; 17 | 18 | //array of accepted globals 19 | globals?: any[]; 20 | 21 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 22 | reporter?: any; 23 | 24 | // bail on the first test failure 25 | bail?: boolean; 26 | 27 | // ignore global leaks 28 | ignoreLeaks?: boolean; 29 | 30 | // grep string or regexp to filter tests with 31 | grep?: any; 32 | } 33 | 34 | interface MochaDone { 35 | (error?: Error): void; 36 | } 37 | 38 | declare var mocha: Mocha; 39 | declare var describe: Mocha.IContextDefinition; 40 | declare var xdescribe: Mocha.IContextDefinition; 41 | // alias for `describe` 42 | declare var context: Mocha.IContextDefinition; 43 | // alias for `describe` 44 | declare var suite: Mocha.IContextDefinition; 45 | declare var it: Mocha.ITestDefinition; 46 | declare var xit: Mocha.ITestDefinition; 47 | // alias for `it` 48 | declare var test: Mocha.ITestDefinition; 49 | 50 | declare function before(action: () => void): void; 51 | 52 | declare function before(action: (done: MochaDone) => void): void; 53 | 54 | declare function before(description: string, action: () => void): void; 55 | 56 | declare function before(description: string, action: (done: MochaDone) => void): void; 57 | 58 | declare function setup(action: () => void): void; 59 | 60 | declare function setup(action: (done: MochaDone) => void): void; 61 | 62 | declare function after(action: () => void): void; 63 | 64 | declare function after(action: (done: MochaDone) => void): void; 65 | 66 | declare function after(description: string, action: () => void): void; 67 | 68 | declare function after(description: string, action: (done: MochaDone) => void): void; 69 | 70 | declare function teardown(action: () => void): void; 71 | 72 | declare function teardown(action: (done: MochaDone) => void): void; 73 | 74 | declare function beforeEach(action: () => void): void; 75 | 76 | declare function beforeEach(action: (done: MochaDone) => void): void; 77 | 78 | declare function beforeEach(description: string, action: () => void): void; 79 | 80 | declare function beforeEach(description: string, action: (done: MochaDone) => void): void; 81 | 82 | declare function suiteSetup(action: () => void): void; 83 | 84 | declare function suiteSetup(action: (done: MochaDone) => void): void; 85 | 86 | declare function afterEach(action: () => void): void; 87 | 88 | declare function afterEach(action: (done: MochaDone) => void): void; 89 | 90 | declare function afterEach(description: string, action: () => void): void; 91 | 92 | declare function afterEach(description: string, action: (done: MochaDone) => void): void; 93 | 94 | declare function suiteTeardown(action: () => void): void; 95 | 96 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 97 | 98 | declare class Mocha { 99 | constructor(options?: { 100 | grep?: RegExp; 101 | ui?: string; 102 | reporter?: string; 103 | timeout?: number; 104 | bail?: boolean; 105 | }); 106 | 107 | /** Setup mocha with the given options. */ 108 | setup(options: MochaSetupOptions): Mocha; 109 | bail(value?: boolean): Mocha; 110 | addFile(file: string): Mocha; 111 | /** Sets reporter by name, defaults to "spec". */ 112 | reporter(name: string): Mocha; 113 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 114 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 115 | ui(value: string): Mocha; 116 | grep(value: string): Mocha; 117 | grep(value: RegExp): Mocha; 118 | invert(): Mocha; 119 | ignoreLeaks(value: boolean): Mocha; 120 | checkLeaks(): Mocha; 121 | /** 122 | * Function to allow assertion libraries to throw errors directly into mocha. 123 | * This is useful when running tests in a browser because window.onerror will 124 | * only receive the 'message' attribute of the Error. 125 | */ 126 | throwError(error: Error): void; 127 | /** Enables growl support. */ 128 | growl(): Mocha; 129 | globals(value: string): Mocha; 130 | globals(values: string[]): Mocha; 131 | useColors(value: boolean): Mocha; 132 | useInlineDiffs(value: boolean): Mocha; 133 | timeout(value: number): Mocha; 134 | slow(value: number): Mocha; 135 | enableTimeouts(value: boolean): Mocha; 136 | asyncOnly(value: boolean): Mocha; 137 | noHighlighting(value: boolean): Mocha; 138 | /** Runs tests and invokes `onComplete()` when finished. */ 139 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 140 | } 141 | 142 | // merge the Mocha class declaration with a module 143 | declare namespace Mocha { 144 | /** Partial interface for Mocha's `Runnable` class. */ 145 | interface IRunnable { 146 | title: string; 147 | fn: Function; 148 | async: boolean; 149 | sync: boolean; 150 | timedOut: boolean; 151 | } 152 | 153 | /** Partial interface for Mocha's `Suite` class. */ 154 | interface ISuite { 155 | parent: ISuite; 156 | title: string; 157 | 158 | fullTitle(): string; 159 | } 160 | 161 | /** Partial interface for Mocha's `Test` class. */ 162 | interface ITest extends IRunnable { 163 | parent: ISuite; 164 | pending: boolean; 165 | 166 | fullTitle(): string; 167 | } 168 | 169 | /** Partial interface for Mocha's `Runner` class. */ 170 | interface IRunner {} 171 | 172 | interface IContextDefinition { 173 | (description: string, spec: () => void): ISuite; 174 | only(description: string, spec: () => void): ISuite; 175 | skip(description: string, spec: () => void): void; 176 | timeout(ms: number): void; 177 | } 178 | 179 | interface ITestDefinition { 180 | (expectation: string, assertion?: () => void): ITest; 181 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 182 | only(expectation: string, assertion?: () => void): ITest; 183 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 184 | skip(expectation: string, assertion?: () => void): void; 185 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 186 | timeout(ms: number): void; 187 | } 188 | 189 | export module reporters { 190 | export class Base { 191 | stats: { 192 | suites: number; 193 | tests: number; 194 | passes: number; 195 | pending: number; 196 | failures: number; 197 | }; 198 | 199 | constructor(runner: IRunner); 200 | } 201 | 202 | export class Doc extends Base {} 203 | export class Dot extends Base {} 204 | export class HTML extends Base {} 205 | export class HTMLCov extends Base {} 206 | export class JSON extends Base {} 207 | export class JSONCov extends Base {} 208 | export class JSONStream extends Base {} 209 | export class Landing extends Base {} 210 | export class List extends Base {} 211 | export class Markdown extends Base {} 212 | export class Min extends Base {} 213 | export class Nyan extends Base {} 214 | export class Progress extends Base { 215 | /** 216 | * @param options.open String used to indicate the start of the progress bar. 217 | * @param options.complete String used to indicate a complete test on the progress bar. 218 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 219 | * @param options.close String used to indicate the end of the progress bar. 220 | */ 221 | constructor(runner: IRunner, options?: { 222 | open?: string; 223 | complete?: string; 224 | incomplete?: string; 225 | close?: string; 226 | }); 227 | } 228 | export class Spec extends Base {} 229 | export class TAP extends Base {} 230 | export class XUnit extends Base { 231 | constructor(runner: IRunner, options?: any); 232 | } 233 | } 234 | } 235 | 236 | declare module "mocha" { 237 | export = Mocha; 238 | } -------------------------------------------------------------------------------- /typings/browser/ambient/mocha/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/mocha/mocha.d.ts 3 | // Type definitions for mocha 2.2.5 4 | // Project: http://mochajs.org/ 5 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 , Vadim Macagon 6 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 7 | 8 | interface MochaSetupOptions { 9 | //milliseconds to wait before considering a test slow 10 | slow?: number; 11 | 12 | // timeout in milliseconds 13 | timeout?: number; 14 | 15 | // ui name "bdd", "tdd", "exports" etc 16 | ui?: string; 17 | 18 | //array of accepted globals 19 | globals?: any[]; 20 | 21 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 22 | reporter?: any; 23 | 24 | // bail on the first test failure 25 | bail?: boolean; 26 | 27 | // ignore global leaks 28 | ignoreLeaks?: boolean; 29 | 30 | // grep string or regexp to filter tests with 31 | grep?: any; 32 | } 33 | 34 | interface MochaDone { 35 | (error?: Error): void; 36 | } 37 | 38 | declare var mocha: Mocha; 39 | declare var describe: Mocha.IContextDefinition; 40 | declare var xdescribe: Mocha.IContextDefinition; 41 | // alias for `describe` 42 | declare var context: Mocha.IContextDefinition; 43 | // alias for `describe` 44 | declare var suite: Mocha.IContextDefinition; 45 | declare var it: Mocha.ITestDefinition; 46 | declare var xit: Mocha.ITestDefinition; 47 | // alias for `it` 48 | declare var test: Mocha.ITestDefinition; 49 | 50 | declare function before(action: () => void): void; 51 | 52 | declare function before(action: (done: MochaDone) => void): void; 53 | 54 | declare function before(description: string, action: () => void): void; 55 | 56 | declare function before(description: string, action: (done: MochaDone) => void): void; 57 | 58 | declare function setup(action: () => void): void; 59 | 60 | declare function setup(action: (done: MochaDone) => void): void; 61 | 62 | declare function after(action: () => void): void; 63 | 64 | declare function after(action: (done: MochaDone) => void): void; 65 | 66 | declare function after(description: string, action: () => void): void; 67 | 68 | declare function after(description: string, action: (done: MochaDone) => void): void; 69 | 70 | declare function teardown(action: () => void): void; 71 | 72 | declare function teardown(action: (done: MochaDone) => void): void; 73 | 74 | declare function beforeEach(action: () => void): void; 75 | 76 | declare function beforeEach(action: (done: MochaDone) => void): void; 77 | 78 | declare function beforeEach(description: string, action: () => void): void; 79 | 80 | declare function beforeEach(description: string, action: (done: MochaDone) => void): void; 81 | 82 | declare function suiteSetup(action: () => void): void; 83 | 84 | declare function suiteSetup(action: (done: MochaDone) => void): void; 85 | 86 | declare function afterEach(action: () => void): void; 87 | 88 | declare function afterEach(action: (done: MochaDone) => void): void; 89 | 90 | declare function afterEach(description: string, action: () => void): void; 91 | 92 | declare function afterEach(description: string, action: (done: MochaDone) => void): void; 93 | 94 | declare function suiteTeardown(action: () => void): void; 95 | 96 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 97 | 98 | declare class Mocha { 99 | constructor(options?: { 100 | grep?: RegExp; 101 | ui?: string; 102 | reporter?: string; 103 | timeout?: number; 104 | bail?: boolean; 105 | }); 106 | 107 | /** Setup mocha with the given options. */ 108 | setup(options: MochaSetupOptions): Mocha; 109 | bail(value?: boolean): Mocha; 110 | addFile(file: string): Mocha; 111 | /** Sets reporter by name, defaults to "spec". */ 112 | reporter(name: string): Mocha; 113 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 114 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 115 | ui(value: string): Mocha; 116 | grep(value: string): Mocha; 117 | grep(value: RegExp): Mocha; 118 | invert(): Mocha; 119 | ignoreLeaks(value: boolean): Mocha; 120 | checkLeaks(): Mocha; 121 | /** 122 | * Function to allow assertion libraries to throw errors directly into mocha. 123 | * This is useful when running tests in a browser because window.onerror will 124 | * only receive the 'message' attribute of the Error. 125 | */ 126 | throwError(error: Error): void; 127 | /** Enables growl support. */ 128 | growl(): Mocha; 129 | globals(value: string): Mocha; 130 | globals(values: string[]): Mocha; 131 | useColors(value: boolean): Mocha; 132 | useInlineDiffs(value: boolean): Mocha; 133 | timeout(value: number): Mocha; 134 | slow(value: number): Mocha; 135 | enableTimeouts(value: boolean): Mocha; 136 | asyncOnly(value: boolean): Mocha; 137 | noHighlighting(value: boolean): Mocha; 138 | /** Runs tests and invokes `onComplete()` when finished. */ 139 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 140 | } 141 | 142 | // merge the Mocha class declaration with a module 143 | declare namespace Mocha { 144 | /** Partial interface for Mocha's `Runnable` class. */ 145 | interface IRunnable { 146 | title: string; 147 | fn: Function; 148 | async: boolean; 149 | sync: boolean; 150 | timedOut: boolean; 151 | } 152 | 153 | /** Partial interface for Mocha's `Suite` class. */ 154 | interface ISuite { 155 | parent: ISuite; 156 | title: string; 157 | 158 | fullTitle(): string; 159 | } 160 | 161 | /** Partial interface for Mocha's `Test` class. */ 162 | interface ITest extends IRunnable { 163 | parent: ISuite; 164 | pending: boolean; 165 | 166 | fullTitle(): string; 167 | } 168 | 169 | /** Partial interface for Mocha's `Runner` class. */ 170 | interface IRunner {} 171 | 172 | interface IContextDefinition { 173 | (description: string, spec: () => void): ISuite; 174 | only(description: string, spec: () => void): ISuite; 175 | skip(description: string, spec: () => void): void; 176 | timeout(ms: number): void; 177 | } 178 | 179 | interface ITestDefinition { 180 | (expectation: string, assertion?: () => void): ITest; 181 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 182 | only(expectation: string, assertion?: () => void): ITest; 183 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 184 | skip(expectation: string, assertion?: () => void): void; 185 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 186 | timeout(ms: number): void; 187 | } 188 | 189 | export module reporters { 190 | export class Base { 191 | stats: { 192 | suites: number; 193 | tests: number; 194 | passes: number; 195 | pending: number; 196 | failures: number; 197 | }; 198 | 199 | constructor(runner: IRunner); 200 | } 201 | 202 | export class Doc extends Base {} 203 | export class Dot extends Base {} 204 | export class HTML extends Base {} 205 | export class HTMLCov extends Base {} 206 | export class JSON extends Base {} 207 | export class JSONCov extends Base {} 208 | export class JSONStream extends Base {} 209 | export class Landing extends Base {} 210 | export class List extends Base {} 211 | export class Markdown extends Base {} 212 | export class Min extends Base {} 213 | export class Nyan extends Base {} 214 | export class Progress extends Base { 215 | /** 216 | * @param options.open String used to indicate the start of the progress bar. 217 | * @param options.complete String used to indicate a complete test on the progress bar. 218 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 219 | * @param options.close String used to indicate the end of the progress bar. 220 | */ 221 | constructor(runner: IRunner, options?: { 222 | open?: string; 223 | complete?: string; 224 | incomplete?: string; 225 | close?: string; 226 | }); 227 | } 228 | export class Spec extends Base {} 229 | export class TAP extends Base {} 230 | export class XUnit extends Base { 231 | constructor(runner: IRunner, options?: any); 232 | } 233 | } 234 | } 235 | 236 | declare module "mocha" { 237 | export = Mocha; 238 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /test/KSeq.tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import {KSeq, OpKind, InsertOp, RemoveOp} from '../src'; 5 | import {Ident} from '../src/idents'; 6 | import {assert} from 'chai'; 7 | 8 | function getWallTime(): number { 9 | return Math.floor(new Date().valueOf() / 1000); 10 | } 11 | 12 | function randomize(arr) { 13 | for (let i = arr.length - 1; i > 0; i--) { 14 | let j = Math.floor(Math.random() * (i + 1)); 15 | let temp = arr[i]; 16 | arr[i] = arr[j]; 17 | arr[j] = temp; 18 | } 19 | } 20 | 21 | describe("KSeq", () => { 22 | 23 | //-------------------------------------------------------------------------------- 24 | 25 | describe("constructor()", () => { 26 | 27 | it("returns a KSeq instance", () => { 28 | let seq = new KSeq("test"); 29 | assert.isNotNull(seq); 30 | }); 31 | 32 | it("assigns the replica id to the KSeq instance", () => { 33 | let name = Math.floor(Math.random() * 100000).toString(); 34 | let seq = new KSeq(name); 35 | assert.equal(name, seq.name); 36 | }); 37 | 38 | }); 39 | 40 | //-------------------------------------------------------------------------------- 41 | 42 | describe("insert()", () => { 43 | 44 | describe("when the position is negative", () => { 45 | 46 | it("throws a RangeError", () => { 47 | let seq = new KSeq("test"); 48 | let func = () => { 49 | seq.insert('a', -1); 50 | }; 51 | assert.throws(func, RangeError); 52 | }); 53 | 54 | }); 55 | 56 | it("returns a valid InsertOp for the operation", () => { 57 | let seq = new KSeq("test"); 58 | let op = seq.insert('a', 0); 59 | assert.equal(op.kind, OpKind.Insert); 60 | assert.equal(op.value, 'a'); 61 | }); 62 | 63 | it("can add an atom to the end of the sequence", () => { 64 | let seq = new KSeq("test"); 65 | seq.insert('a', 0); 66 | seq.insert('b', 1); 67 | assert.equal(seq.size(), 2); 68 | assert.equal(seq.get(0), 'a'); 69 | assert.equal(seq.get(1), 'b'); 70 | }); 71 | 72 | it("can add an atom to the beginning of the sequence", () => { 73 | let seq = new KSeq("test"); 74 | seq.insert('a', 0); 75 | seq.insert('b', 1); 76 | seq.insert('c', 0); 77 | assert.equal(seq.size(), 3); 78 | assert.equal(seq.get(0), 'c'); 79 | assert.equal(seq.get(1), 'a'); 80 | assert.equal(seq.get(2), 'b'); 81 | }); 82 | 83 | it("can add 1000 items at the end", () => { 84 | let seq = new KSeq("test"); 85 | for (let i = 1; i <= 1000; i++) { 86 | seq.insert(i, seq.size()); 87 | } 88 | assert.equal(seq.size(), 1000); 89 | assert.equal(seq.get(seq.size() - 1), 1000); 90 | }); 91 | 92 | it("can add 1000 items at the beginning", () => { 93 | let seq = new KSeq("test"); 94 | for (let i = 1; i <= 1000; i++) { 95 | seq.insert(i, 0); 96 | } 97 | assert.equal(seq.size(), 1000); 98 | assert.equal(seq.get(0), 1000); 99 | }); 100 | 101 | }); 102 | 103 | //-------------------------------------------------------------------------------- 104 | 105 | describe("remove()", () => { 106 | 107 | describe("when the position is negative", () => { 108 | 109 | it("throws a RangeError", () => { 110 | let seq = new KSeq("test"); 111 | let func = () => { 112 | seq.remove(-1); 113 | }; 114 | assert.throws(func, RangeError); 115 | }); 116 | 117 | }); 118 | 119 | describe("when the position is out of range", () => { 120 | 121 | it("returns null", () => { 122 | let seq = new KSeq("test"); 123 | let ret = seq.remove(100); 124 | assert.equal(ret, null); 125 | }); 126 | 127 | }); 128 | 129 | describe("when an atom exists at the position", () => { 130 | 131 | it("returns a valid RemoveOp for the operation", () => { 132 | let seq = new KSeq("test"); 133 | seq.insert('a', 0); 134 | assert.equal(seq.get(0), 'a'); 135 | let op = seq.remove(0); 136 | assert.equal(op.kind, OpKind.Remove); 137 | }); 138 | 139 | it("removes the atom at that position", () => { 140 | let seq = new KSeq("test"); 141 | seq.insert('a', 0); 142 | seq.insert('b', 1); 143 | seq.insert('c', 2); 144 | assert.equal(seq.size(), 3); 145 | seq.remove(1); 146 | assert.equal(seq.size(), 2); 147 | assert.equal(seq.get(1), 'c'); 148 | }); 149 | 150 | }); 151 | 152 | }); 153 | 154 | //-------------------------------------------------------------------------------- 155 | 156 | describe("apply()", () => { 157 | 158 | describe("when passed an InsertOp", () => { 159 | 160 | describe("when the specified atom does not exist", () => { 161 | 162 | it("adds the atom", () => { 163 | let seq1 = new KSeq("alice"); 164 | let seq2 = new KSeq("bob"); 165 | let op = seq1.insert('a', 0); 166 | seq2.apply(op); 167 | assert.equal(seq2.size(), 1); 168 | assert.equal(seq2.get(0), 'a'); 169 | }); 170 | 171 | }); 172 | 173 | describe("when an atom with the specified ident already exists", () => { 174 | 175 | it("ignores the operation", () => { 176 | let seq = new KSeq("test"); 177 | let op1 = seq.insert(42, 0); 178 | assert.equal(seq.size(), 1); 179 | assert.equal(seq.get(0), 42); 180 | let op2 = new InsertOp("test", getWallTime(), op1.id, 99); 181 | seq.apply(op2); 182 | assert.equal(seq.size(), 1); 183 | }); 184 | 185 | }); 186 | 187 | describe("when an atom with the specified ident has already been removed", () => { 188 | 189 | it("ignores the operation", () => { 190 | let seq = new KSeq("test"); 191 | let op1 = seq.append(42); 192 | seq.append(99); 193 | seq.remove(0); 194 | assert.equal(seq.size(), 1); 195 | let op2 = new InsertOp("test", getWallTime(), op1.id, 123); 196 | seq.apply(op2); 197 | assert.equal(seq.size(), 1); 198 | }); 199 | 200 | }); 201 | 202 | }); 203 | 204 | describe("when passed a RemoveOp", () => { 205 | 206 | describe("when the specified atom does not exist", () => { 207 | 208 | it("ignores the operation", () => { 209 | let seq1 = new KSeq("alice"); 210 | let seq2 = new KSeq("bob"); 211 | seq1.insert('a', 0); 212 | seq2.insert('b', 0); 213 | let op = seq1.remove(0); 214 | assert.isNotNull(op); 215 | seq2.apply(op); 216 | assert.equal(seq2.size(), 1); 217 | assert.equal(seq2.get(0), 'b'); 218 | }); 219 | 220 | }); 221 | 222 | describe("when the specified atom exists", () => { 223 | 224 | it("removes the atom", () => { 225 | let seq1 = new KSeq("alice"); 226 | let seq2 = new KSeq("bob"); 227 | let insertOp = seq1.insert(42, 0); 228 | seq2.apply(insertOp); 229 | assert.equal(seq1.size(), seq2.size()); 230 | assert.equal(seq1.get(0), seq2.get(0)); 231 | let removeOp = seq2.remove(0); 232 | seq1.apply(removeOp); 233 | assert.equal(seq1.size(), seq2.size()); 234 | assert.equal(seq1.get(0), seq2.get(0)); 235 | }); 236 | 237 | }); 238 | 239 | describe("when an atom with the specified ident has already been removed", () => { 240 | 241 | it("ignores the operation", () => { 242 | let seq = new KSeq("test"); 243 | let op1 = seq.append(42); 244 | seq.append(99); 245 | seq.remove(0); 246 | assert.equal(seq.size(), 1); 247 | let op2 = new RemoveOp("test", getWallTime(), op1.id); 248 | seq.apply(op2); 249 | assert.equal(seq.size(), 1); 250 | }); 251 | 252 | }); 253 | 254 | }); 255 | 256 | }); 257 | 258 | //-------------------------------------------------------------------------------- 259 | 260 | describe("is a CRDT", () => { 261 | 262 | it("supports insert/remove (base case)", () => { 263 | let seq = new KSeq("alice"); 264 | seq.append("test"); 265 | assert.equal(seq.size(), 1); 266 | let ident = Ident.parse("1#0:bob"); 267 | let op1 = new InsertOp("bob", getWallTime(), ident, "hello"); 268 | let op2 = new RemoveOp("bob", getWallTime(), ident); 269 | seq.apply(op1); 270 | assert.equal(seq.size(), 2); 271 | seq.apply(op2); 272 | assert.equal(seq.size(), 1); 273 | }); 274 | 275 | it("idempotence: supports insert/insert (message duplication)", () => { 276 | let seq = new KSeq("alice"); 277 | seq.append("test"); 278 | assert.equal(seq.size(), 1); 279 | let ident = Ident.parse("1#0:bob"); 280 | let op = new InsertOp("bob", getWallTime(), ident, "hello"); 281 | seq.apply(op); 282 | assert.equal(seq.size(), 2); 283 | seq.apply(op); 284 | assert.equal(seq.size(), 2); 285 | }); 286 | 287 | it("idempotence: supports remove/remove (message duplication)", () => { 288 | let seq = new KSeq("alice"); 289 | seq.append("test"); 290 | assert.equal(seq.size(), 1); 291 | let ident = Ident.parse("1#0:bob"); 292 | let op1 = new InsertOp("bob", getWallTime(), ident, "hello"); 293 | let op2 = new RemoveOp("bob", getWallTime(), ident); 294 | seq.apply(op1); 295 | assert.equal(seq.size(), 2); 296 | seq.apply(op2); 297 | assert.equal(seq.size(), 1); 298 | seq.apply(op2); 299 | assert.equal(seq.size(), 1); 300 | }); 301 | 302 | it("commutative: supports remove/insert (messages out of order)", () => { 303 | let seq = new KSeq("alice"); 304 | seq.append("test"); 305 | assert.equal(seq.size(), 1); 306 | let ident = Ident.parse("1#0:bob"); 307 | let op1 = new InsertOp("bob", getWallTime(), ident, "hello"); 308 | let op2 = new RemoveOp("bob", getWallTime(), ident); 309 | seq.apply(op2); 310 | assert.equal(seq.size(), 1); 311 | seq.apply(op1); 312 | assert.equal(seq.size(), 1); 313 | }); 314 | 315 | it("replicas converge when issued many randomized operations from a single replica", () => { 316 | let alice = new KSeq("alice"); 317 | let bob = new KSeq("bob"); 318 | let ops = []; 319 | 320 | for (let i = 0; i < 1000; i++) { 321 | let pos = Math.floor(Math.random() * i); 322 | ops.push(alice.insert(i, pos)); 323 | } 324 | for (let i = 0; i < 500; i++) { 325 | let pos = Math.floor(Math.random() * (1000 - i)); 326 | ops.push(alice.remove(pos)); 327 | } 328 | 329 | randomize(ops); 330 | 331 | for (let i = 0; i < ops.length; i++) { 332 | bob.apply(ops[i]); 333 | } 334 | 335 | assert.equal(alice.size(), 500); 336 | assert.equal(bob.size(), 500); 337 | assert.deepEqual(alice.toArray(), bob.toArray()); 338 | }); 339 | 340 | it("replicas converge when issued many randomized operations from multiple replicas", () => { 341 | let alice = new KSeq("alice"); 342 | let bob = new KSeq("bob"); 343 | 344 | let aliceOps = []; 345 | let bobOps = []; 346 | for (let i = 0; i < 1000; i++) { 347 | aliceOps.push(alice.insert(i, Math.floor(Math.random() * i))); 348 | bobOps.push(bob.insert(i, Math.floor(Math.random() * i))); 349 | } 350 | for (let i = 0; i < 500; i++) { 351 | aliceOps.push(alice.remove(Math.floor(Math.random() * (1000 - i)))); 352 | bobOps.push(bob.remove(Math.floor(Math.random() * (1000 - i)))); 353 | } 354 | 355 | randomize(aliceOps); 356 | randomize(bobOps); 357 | 358 | for (let i = 0; i < aliceOps.length; i++) { 359 | bob.apply(aliceOps[i]); 360 | } 361 | for (let i = 0; i < bobOps.length; i++) { 362 | alice.apply(bobOps[i]); 363 | } 364 | 365 | assert.equal(alice.size(), bob.size()); 366 | assert.deepEqual(alice.toArray(), bob.toArray()); 367 | }); 368 | 369 | }); 370 | 371 | //-------------------------------------------------------------------------------- 372 | 373 | }); -------------------------------------------------------------------------------- /typings/main/ambient/chai/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/chai/chai.d.ts 3 | // Type definitions for chai 3.4.0 4 | // Project: http://chaijs.com/ 5 | // Definitions by: Jed Mao , 6 | // Bart van der Schoor , 7 | // Andrew Brown , 8 | // Olivier Chevet , 9 | // Matt Wistrand 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | 12 | // 13 | 14 | declare namespace Chai { 15 | 16 | interface ChaiStatic { 17 | expect: ExpectStatic; 18 | should(): Should; 19 | /** 20 | * Provides a way to extend the internals of Chai 21 | */ 22 | use(fn: (chai: any, utils: any) => void): ChaiStatic; 23 | assert: AssertStatic; 24 | config: Config; 25 | AssertionError: typeof AssertionError; 26 | } 27 | 28 | export interface ExpectStatic extends AssertionStatic { 29 | fail(actual?: any, expected?: any, message?: string, operator?: string): void; 30 | } 31 | 32 | export interface AssertStatic extends Assert { 33 | } 34 | 35 | export interface AssertionStatic { 36 | (target: any, message?: string): Assertion; 37 | } 38 | 39 | interface ShouldAssertion { 40 | equal(value1: any, value2: any, message?: string): void; 41 | Throw: ShouldThrow; 42 | throw: ShouldThrow; 43 | exist(value: any, message?: string): void; 44 | } 45 | 46 | interface Should extends ShouldAssertion { 47 | not: ShouldAssertion; 48 | fail(actual: any, expected: any, message?: string, operator?: string): void; 49 | } 50 | 51 | interface ShouldThrow { 52 | (actual: Function): void; 53 | (actual: Function, expected: string|RegExp, message?: string): void; 54 | (actual: Function, constructor: Error|Function, expected?: string|RegExp, message?: string): void; 55 | } 56 | 57 | interface Assertion extends LanguageChains, NumericComparison, TypeComparison { 58 | not: Assertion; 59 | deep: Deep; 60 | any: KeyFilter; 61 | all: KeyFilter; 62 | a: TypeComparison; 63 | an: TypeComparison; 64 | include: Include; 65 | includes: Include; 66 | contain: Include; 67 | contains: Include; 68 | ok: Assertion; 69 | true: Assertion; 70 | false: Assertion; 71 | null: Assertion; 72 | undefined: Assertion; 73 | NaN: Assertion; 74 | exist: Assertion; 75 | empty: Assertion; 76 | arguments: Assertion; 77 | Arguments: Assertion; 78 | equal: Equal; 79 | equals: Equal; 80 | eq: Equal; 81 | eql: Equal; 82 | eqls: Equal; 83 | property: Property; 84 | ownProperty: OwnProperty; 85 | haveOwnProperty: OwnProperty; 86 | ownPropertyDescriptor: OwnPropertyDescriptor; 87 | haveOwnPropertyDescriptor: OwnPropertyDescriptor; 88 | length: Length; 89 | lengthOf: Length; 90 | match: Match; 91 | matches: Match; 92 | string(string: string, message?: string): Assertion; 93 | keys: Keys; 94 | key(string: string): Assertion; 95 | throw: Throw; 96 | throws: Throw; 97 | Throw: Throw; 98 | respondTo: RespondTo; 99 | respondsTo: RespondTo; 100 | itself: Assertion; 101 | satisfy: Satisfy; 102 | satisfies: Satisfy; 103 | closeTo: CloseTo; 104 | approximately: CloseTo; 105 | members: Members; 106 | increase: PropertyChange; 107 | increases: PropertyChange; 108 | decrease: PropertyChange; 109 | decreases: PropertyChange; 110 | change: PropertyChange; 111 | changes: PropertyChange; 112 | extensible: Assertion; 113 | sealed: Assertion; 114 | frozen: Assertion; 115 | oneOf(list: any[], message?: string): Assertion; 116 | } 117 | 118 | interface LanguageChains { 119 | to: Assertion; 120 | be: Assertion; 121 | been: Assertion; 122 | is: Assertion; 123 | that: Assertion; 124 | which: Assertion; 125 | and: Assertion; 126 | has: Assertion; 127 | have: Assertion; 128 | with: Assertion; 129 | at: Assertion; 130 | of: Assertion; 131 | same: Assertion; 132 | } 133 | 134 | interface NumericComparison { 135 | above: NumberComparer; 136 | gt: NumberComparer; 137 | greaterThan: NumberComparer; 138 | least: NumberComparer; 139 | gte: NumberComparer; 140 | below: NumberComparer; 141 | lt: NumberComparer; 142 | lessThan: NumberComparer; 143 | most: NumberComparer; 144 | lte: NumberComparer; 145 | within(start: number, finish: number, message?: string): Assertion; 146 | } 147 | 148 | interface NumberComparer { 149 | (value: number, message?: string): Assertion; 150 | } 151 | 152 | interface TypeComparison { 153 | (type: string, message?: string): Assertion; 154 | instanceof: InstanceOf; 155 | instanceOf: InstanceOf; 156 | } 157 | 158 | interface InstanceOf { 159 | (constructor: Object, message?: string): Assertion; 160 | } 161 | 162 | interface CloseTo { 163 | (expected: number, delta: number, message?: string): Assertion; 164 | } 165 | 166 | interface Deep { 167 | equal: Equal; 168 | include: Include; 169 | property: Property; 170 | members: Members; 171 | } 172 | 173 | interface KeyFilter { 174 | keys: Keys; 175 | } 176 | 177 | interface Equal { 178 | (value: any, message?: string): Assertion; 179 | } 180 | 181 | interface Property { 182 | (name: string, value?: any, message?: string): Assertion; 183 | } 184 | 185 | interface OwnProperty { 186 | (name: string, message?: string): Assertion; 187 | } 188 | 189 | interface OwnPropertyDescriptor { 190 | (name: string, descriptor: PropertyDescriptor, message?: string): Assertion; 191 | (name: string, message?: string): Assertion; 192 | } 193 | 194 | interface Length extends LanguageChains, NumericComparison { 195 | (length: number, message?: string): Assertion; 196 | } 197 | 198 | interface Include { 199 | (value: Object, message?: string): Assertion; 200 | (value: string, message?: string): Assertion; 201 | (value: number, message?: string): Assertion; 202 | keys: Keys; 203 | members: Members; 204 | any: KeyFilter; 205 | all: KeyFilter; 206 | } 207 | 208 | interface Match { 209 | (regexp: RegExp|string, message?: string): Assertion; 210 | } 211 | 212 | interface Keys { 213 | (...keys: string[]): Assertion; 214 | (keys: any[]): Assertion; 215 | (keys: Object): Assertion; 216 | } 217 | 218 | interface Throw { 219 | (): Assertion; 220 | (expected: string, message?: string): Assertion; 221 | (expected: RegExp, message?: string): Assertion; 222 | (constructor: Error, expected?: string, message?: string): Assertion; 223 | (constructor: Error, expected?: RegExp, message?: string): Assertion; 224 | (constructor: Function, expected?: string, message?: string): Assertion; 225 | (constructor: Function, expected?: RegExp, message?: string): Assertion; 226 | } 227 | 228 | interface RespondTo { 229 | (method: string, message?: string): Assertion; 230 | } 231 | 232 | interface Satisfy { 233 | (matcher: Function, message?: string): Assertion; 234 | } 235 | 236 | interface Members { 237 | (set: any[], message?: string): Assertion; 238 | } 239 | 240 | interface PropertyChange { 241 | (object: Object, prop: string, msg?: string): Assertion; 242 | } 243 | 244 | export interface Assert { 245 | /** 246 | * @param expression Expression to test for truthiness. 247 | * @param message Message to display on error. 248 | */ 249 | (expression: any, message?: string): void; 250 | 251 | fail(actual?: any, expected?: any, msg?: string, operator?: string): void; 252 | 253 | ok(val: any, msg?: string): void; 254 | isOk(val: any, msg?: string): void; 255 | notOk(val: any, msg?: string): void; 256 | isNotOk(val: any, msg?: string): void; 257 | 258 | equal(act: any, exp: any, msg?: string): void; 259 | notEqual(act: any, exp: any, msg?: string): void; 260 | 261 | strictEqual(act: any, exp: any, msg?: string): void; 262 | notStrictEqual(act: any, exp: any, msg?: string): void; 263 | 264 | deepEqual(act: any, exp: any, msg?: string): void; 265 | notDeepEqual(act: any, exp: any, msg?: string): void; 266 | 267 | isTrue(val: any, msg?: string): void; 268 | isFalse(val: any, msg?: string): void; 269 | 270 | isNotTrue(val: any, msg?: string): void; 271 | isNotFalse(val: any, msg?: string): void; 272 | 273 | isNull(val: any, msg?: string): void; 274 | isNotNull(val: any, msg?: string): void; 275 | 276 | isUndefined(val: any, msg?: string): void; 277 | isDefined(val: any, msg?: string): void; 278 | 279 | isNaN(val: any, msg?: string): void; 280 | isNotNaN(val: any, msg?: string): void; 281 | 282 | isAbove(val: number, abv: number, msg?: string): void; 283 | isBelow(val: number, blw: number, msg?: string): void; 284 | 285 | isAtLeast(val: number, atlst: number, msg?: string): void; 286 | isAtMost(val: number, atmst: number, msg?: string): void; 287 | 288 | isFunction(val: any, msg?: string): void; 289 | isNotFunction(val: any, msg?: string): void; 290 | 291 | isObject(val: any, msg?: string): void; 292 | isNotObject(val: any, msg?: string): void; 293 | 294 | isArray(val: any, msg?: string): void; 295 | isNotArray(val: any, msg?: string): void; 296 | 297 | isString(val: any, msg?: string): void; 298 | isNotString(val: any, msg?: string): void; 299 | 300 | isNumber(val: any, msg?: string): void; 301 | isNotNumber(val: any, msg?: string): void; 302 | 303 | isBoolean(val: any, msg?: string): void; 304 | isNotBoolean(val: any, msg?: string): void; 305 | 306 | typeOf(val: any, type: string, msg?: string): void; 307 | notTypeOf(val: any, type: string, msg?: string): void; 308 | 309 | instanceOf(val: any, type: Function, msg?: string): void; 310 | notInstanceOf(val: any, type: Function, msg?: string): void; 311 | 312 | include(exp: string, inc: any, msg?: string): void; 313 | include(exp: any[], inc: any, msg?: string): void; 314 | 315 | notInclude(exp: string, inc: any, msg?: string): void; 316 | notInclude(exp: any[], inc: any, msg?: string): void; 317 | 318 | match(exp: any, re: RegExp, msg?: string): void; 319 | notMatch(exp: any, re: RegExp, msg?: string): void; 320 | 321 | property(obj: Object, prop: string, msg?: string): void; 322 | notProperty(obj: Object, prop: string, msg?: string): void; 323 | deepProperty(obj: Object, prop: string, msg?: string): void; 324 | notDeepProperty(obj: Object, prop: string, msg?: string): void; 325 | 326 | propertyVal(obj: Object, prop: string, val: any, msg?: string): void; 327 | propertyNotVal(obj: Object, prop: string, val: any, msg?: string): void; 328 | 329 | deepPropertyVal(obj: Object, prop: string, val: any, msg?: string): void; 330 | deepPropertyNotVal(obj: Object, prop: string, val: any, msg?: string): void; 331 | 332 | lengthOf(exp: any, len: number, msg?: string): void; 333 | //alias frenzy 334 | throw(fn: Function, msg?: string): void; 335 | throw(fn: Function, regExp: RegExp): void; 336 | throw(fn: Function, errType: Function, msg?: string): void; 337 | throw(fn: Function, errType: Function, regExp: RegExp): void; 338 | 339 | throws(fn: Function, msg?: string): void; 340 | throws(fn: Function, regExp: RegExp): void; 341 | throws(fn: Function, errType: Function, msg?: string): void; 342 | throws(fn: Function, errType: Function, regExp: RegExp): void; 343 | 344 | Throw(fn: Function, msg?: string): void; 345 | Throw(fn: Function, regExp: RegExp): void; 346 | Throw(fn: Function, errType: Function, msg?: string): void; 347 | Throw(fn: Function, errType: Function, regExp: RegExp): void; 348 | 349 | doesNotThrow(fn: Function, msg?: string): void; 350 | doesNotThrow(fn: Function, regExp: RegExp): void; 351 | doesNotThrow(fn: Function, errType: Function, msg?: string): void; 352 | doesNotThrow(fn: Function, errType: Function, regExp: RegExp): void; 353 | 354 | operator(val: any, operator: string, val2: any, msg?: string): void; 355 | closeTo(act: number, exp: number, delta: number, msg?: string): void; 356 | approximately(act: number, exp: number, delta: number, msg?: string): void; 357 | 358 | sameMembers(set1: any[], set2: any[], msg?: string): void; 359 | sameDeepMembers(set1: any[], set2: any[], msg?: string): void; 360 | includeMembers(superset: any[], subset: any[], msg?: string): void; 361 | 362 | ifError(val: any, msg?: string): void; 363 | 364 | isExtensible(obj: {}, msg?: string): void; 365 | extensible(obj: {}, msg?: string): void; 366 | isNotExtensible(obj: {}, msg?: string): void; 367 | notExtensible(obj: {}, msg?: string): void; 368 | 369 | isSealed(obj: {}, msg?: string): void; 370 | sealed(obj: {}, msg?: string): void; 371 | isNotSealed(obj: {}, msg?: string): void; 372 | notSealed(obj: {}, msg?: string): void; 373 | 374 | isFrozen(obj: Object, msg?: string): void; 375 | frozen(obj: Object, msg?: string): void; 376 | isNotFrozen(obj: Object, msg?: string): void; 377 | notFrozen(obj: Object, msg?: string): void; 378 | 379 | oneOf(inList: any, list: any[], msg?: string): void; 380 | } 381 | 382 | export interface Config { 383 | includeStack: boolean; 384 | } 385 | 386 | export class AssertionError { 387 | constructor(message: string, _props?: any, ssf?: Function); 388 | name: string; 389 | message: string; 390 | showDiff: boolean; 391 | stack: string; 392 | } 393 | } 394 | 395 | declare var chai: Chai.ChaiStatic; 396 | 397 | declare module "chai" { 398 | export = chai; 399 | } 400 | 401 | interface Object { 402 | should: Chai.Assertion; 403 | } -------------------------------------------------------------------------------- /typings/browser/ambient/chai/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/chai/chai.d.ts 3 | // Type definitions for chai 3.4.0 4 | // Project: http://chaijs.com/ 5 | // Definitions by: Jed Mao , 6 | // Bart van der Schoor , 7 | // Andrew Brown , 8 | // Olivier Chevet , 9 | // Matt Wistrand 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | 12 | // 13 | 14 | declare namespace Chai { 15 | 16 | interface ChaiStatic { 17 | expect: ExpectStatic; 18 | should(): Should; 19 | /** 20 | * Provides a way to extend the internals of Chai 21 | */ 22 | use(fn: (chai: any, utils: any) => void): ChaiStatic; 23 | assert: AssertStatic; 24 | config: Config; 25 | AssertionError: typeof AssertionError; 26 | } 27 | 28 | export interface ExpectStatic extends AssertionStatic { 29 | fail(actual?: any, expected?: any, message?: string, operator?: string): void; 30 | } 31 | 32 | export interface AssertStatic extends Assert { 33 | } 34 | 35 | export interface AssertionStatic { 36 | (target: any, message?: string): Assertion; 37 | } 38 | 39 | interface ShouldAssertion { 40 | equal(value1: any, value2: any, message?: string): void; 41 | Throw: ShouldThrow; 42 | throw: ShouldThrow; 43 | exist(value: any, message?: string): void; 44 | } 45 | 46 | interface Should extends ShouldAssertion { 47 | not: ShouldAssertion; 48 | fail(actual: any, expected: any, message?: string, operator?: string): void; 49 | } 50 | 51 | interface ShouldThrow { 52 | (actual: Function): void; 53 | (actual: Function, expected: string|RegExp, message?: string): void; 54 | (actual: Function, constructor: Error|Function, expected?: string|RegExp, message?: string): void; 55 | } 56 | 57 | interface Assertion extends LanguageChains, NumericComparison, TypeComparison { 58 | not: Assertion; 59 | deep: Deep; 60 | any: KeyFilter; 61 | all: KeyFilter; 62 | a: TypeComparison; 63 | an: TypeComparison; 64 | include: Include; 65 | includes: Include; 66 | contain: Include; 67 | contains: Include; 68 | ok: Assertion; 69 | true: Assertion; 70 | false: Assertion; 71 | null: Assertion; 72 | undefined: Assertion; 73 | NaN: Assertion; 74 | exist: Assertion; 75 | empty: Assertion; 76 | arguments: Assertion; 77 | Arguments: Assertion; 78 | equal: Equal; 79 | equals: Equal; 80 | eq: Equal; 81 | eql: Equal; 82 | eqls: Equal; 83 | property: Property; 84 | ownProperty: OwnProperty; 85 | haveOwnProperty: OwnProperty; 86 | ownPropertyDescriptor: OwnPropertyDescriptor; 87 | haveOwnPropertyDescriptor: OwnPropertyDescriptor; 88 | length: Length; 89 | lengthOf: Length; 90 | match: Match; 91 | matches: Match; 92 | string(string: string, message?: string): Assertion; 93 | keys: Keys; 94 | key(string: string): Assertion; 95 | throw: Throw; 96 | throws: Throw; 97 | Throw: Throw; 98 | respondTo: RespondTo; 99 | respondsTo: RespondTo; 100 | itself: Assertion; 101 | satisfy: Satisfy; 102 | satisfies: Satisfy; 103 | closeTo: CloseTo; 104 | approximately: CloseTo; 105 | members: Members; 106 | increase: PropertyChange; 107 | increases: PropertyChange; 108 | decrease: PropertyChange; 109 | decreases: PropertyChange; 110 | change: PropertyChange; 111 | changes: PropertyChange; 112 | extensible: Assertion; 113 | sealed: Assertion; 114 | frozen: Assertion; 115 | oneOf(list: any[], message?: string): Assertion; 116 | } 117 | 118 | interface LanguageChains { 119 | to: Assertion; 120 | be: Assertion; 121 | been: Assertion; 122 | is: Assertion; 123 | that: Assertion; 124 | which: Assertion; 125 | and: Assertion; 126 | has: Assertion; 127 | have: Assertion; 128 | with: Assertion; 129 | at: Assertion; 130 | of: Assertion; 131 | same: Assertion; 132 | } 133 | 134 | interface NumericComparison { 135 | above: NumberComparer; 136 | gt: NumberComparer; 137 | greaterThan: NumberComparer; 138 | least: NumberComparer; 139 | gte: NumberComparer; 140 | below: NumberComparer; 141 | lt: NumberComparer; 142 | lessThan: NumberComparer; 143 | most: NumberComparer; 144 | lte: NumberComparer; 145 | within(start: number, finish: number, message?: string): Assertion; 146 | } 147 | 148 | interface NumberComparer { 149 | (value: number, message?: string): Assertion; 150 | } 151 | 152 | interface TypeComparison { 153 | (type: string, message?: string): Assertion; 154 | instanceof: InstanceOf; 155 | instanceOf: InstanceOf; 156 | } 157 | 158 | interface InstanceOf { 159 | (constructor: Object, message?: string): Assertion; 160 | } 161 | 162 | interface CloseTo { 163 | (expected: number, delta: number, message?: string): Assertion; 164 | } 165 | 166 | interface Deep { 167 | equal: Equal; 168 | include: Include; 169 | property: Property; 170 | members: Members; 171 | } 172 | 173 | interface KeyFilter { 174 | keys: Keys; 175 | } 176 | 177 | interface Equal { 178 | (value: any, message?: string): Assertion; 179 | } 180 | 181 | interface Property { 182 | (name: string, value?: any, message?: string): Assertion; 183 | } 184 | 185 | interface OwnProperty { 186 | (name: string, message?: string): Assertion; 187 | } 188 | 189 | interface OwnPropertyDescriptor { 190 | (name: string, descriptor: PropertyDescriptor, message?: string): Assertion; 191 | (name: string, message?: string): Assertion; 192 | } 193 | 194 | interface Length extends LanguageChains, NumericComparison { 195 | (length: number, message?: string): Assertion; 196 | } 197 | 198 | interface Include { 199 | (value: Object, message?: string): Assertion; 200 | (value: string, message?: string): Assertion; 201 | (value: number, message?: string): Assertion; 202 | keys: Keys; 203 | members: Members; 204 | any: KeyFilter; 205 | all: KeyFilter; 206 | } 207 | 208 | interface Match { 209 | (regexp: RegExp|string, message?: string): Assertion; 210 | } 211 | 212 | interface Keys { 213 | (...keys: string[]): Assertion; 214 | (keys: any[]): Assertion; 215 | (keys: Object): Assertion; 216 | } 217 | 218 | interface Throw { 219 | (): Assertion; 220 | (expected: string, message?: string): Assertion; 221 | (expected: RegExp, message?: string): Assertion; 222 | (constructor: Error, expected?: string, message?: string): Assertion; 223 | (constructor: Error, expected?: RegExp, message?: string): Assertion; 224 | (constructor: Function, expected?: string, message?: string): Assertion; 225 | (constructor: Function, expected?: RegExp, message?: string): Assertion; 226 | } 227 | 228 | interface RespondTo { 229 | (method: string, message?: string): Assertion; 230 | } 231 | 232 | interface Satisfy { 233 | (matcher: Function, message?: string): Assertion; 234 | } 235 | 236 | interface Members { 237 | (set: any[], message?: string): Assertion; 238 | } 239 | 240 | interface PropertyChange { 241 | (object: Object, prop: string, msg?: string): Assertion; 242 | } 243 | 244 | export interface Assert { 245 | /** 246 | * @param expression Expression to test for truthiness. 247 | * @param message Message to display on error. 248 | */ 249 | (expression: any, message?: string): void; 250 | 251 | fail(actual?: any, expected?: any, msg?: string, operator?: string): void; 252 | 253 | ok(val: any, msg?: string): void; 254 | isOk(val: any, msg?: string): void; 255 | notOk(val: any, msg?: string): void; 256 | isNotOk(val: any, msg?: string): void; 257 | 258 | equal(act: any, exp: any, msg?: string): void; 259 | notEqual(act: any, exp: any, msg?: string): void; 260 | 261 | strictEqual(act: any, exp: any, msg?: string): void; 262 | notStrictEqual(act: any, exp: any, msg?: string): void; 263 | 264 | deepEqual(act: any, exp: any, msg?: string): void; 265 | notDeepEqual(act: any, exp: any, msg?: string): void; 266 | 267 | isTrue(val: any, msg?: string): void; 268 | isFalse(val: any, msg?: string): void; 269 | 270 | isNotTrue(val: any, msg?: string): void; 271 | isNotFalse(val: any, msg?: string): void; 272 | 273 | isNull(val: any, msg?: string): void; 274 | isNotNull(val: any, msg?: string): void; 275 | 276 | isUndefined(val: any, msg?: string): void; 277 | isDefined(val: any, msg?: string): void; 278 | 279 | isNaN(val: any, msg?: string): void; 280 | isNotNaN(val: any, msg?: string): void; 281 | 282 | isAbove(val: number, abv: number, msg?: string): void; 283 | isBelow(val: number, blw: number, msg?: string): void; 284 | 285 | isAtLeast(val: number, atlst: number, msg?: string): void; 286 | isAtMost(val: number, atmst: number, msg?: string): void; 287 | 288 | isFunction(val: any, msg?: string): void; 289 | isNotFunction(val: any, msg?: string): void; 290 | 291 | isObject(val: any, msg?: string): void; 292 | isNotObject(val: any, msg?: string): void; 293 | 294 | isArray(val: any, msg?: string): void; 295 | isNotArray(val: any, msg?: string): void; 296 | 297 | isString(val: any, msg?: string): void; 298 | isNotString(val: any, msg?: string): void; 299 | 300 | isNumber(val: any, msg?: string): void; 301 | isNotNumber(val: any, msg?: string): void; 302 | 303 | isBoolean(val: any, msg?: string): void; 304 | isNotBoolean(val: any, msg?: string): void; 305 | 306 | typeOf(val: any, type: string, msg?: string): void; 307 | notTypeOf(val: any, type: string, msg?: string): void; 308 | 309 | instanceOf(val: any, type: Function, msg?: string): void; 310 | notInstanceOf(val: any, type: Function, msg?: string): void; 311 | 312 | include(exp: string, inc: any, msg?: string): void; 313 | include(exp: any[], inc: any, msg?: string): void; 314 | 315 | notInclude(exp: string, inc: any, msg?: string): void; 316 | notInclude(exp: any[], inc: any, msg?: string): void; 317 | 318 | match(exp: any, re: RegExp, msg?: string): void; 319 | notMatch(exp: any, re: RegExp, msg?: string): void; 320 | 321 | property(obj: Object, prop: string, msg?: string): void; 322 | notProperty(obj: Object, prop: string, msg?: string): void; 323 | deepProperty(obj: Object, prop: string, msg?: string): void; 324 | notDeepProperty(obj: Object, prop: string, msg?: string): void; 325 | 326 | propertyVal(obj: Object, prop: string, val: any, msg?: string): void; 327 | propertyNotVal(obj: Object, prop: string, val: any, msg?: string): void; 328 | 329 | deepPropertyVal(obj: Object, prop: string, val: any, msg?: string): void; 330 | deepPropertyNotVal(obj: Object, prop: string, val: any, msg?: string): void; 331 | 332 | lengthOf(exp: any, len: number, msg?: string): void; 333 | //alias frenzy 334 | throw(fn: Function, msg?: string): void; 335 | throw(fn: Function, regExp: RegExp): void; 336 | throw(fn: Function, errType: Function, msg?: string): void; 337 | throw(fn: Function, errType: Function, regExp: RegExp): void; 338 | 339 | throws(fn: Function, msg?: string): void; 340 | throws(fn: Function, regExp: RegExp): void; 341 | throws(fn: Function, errType: Function, msg?: string): void; 342 | throws(fn: Function, errType: Function, regExp: RegExp): void; 343 | 344 | Throw(fn: Function, msg?: string): void; 345 | Throw(fn: Function, regExp: RegExp): void; 346 | Throw(fn: Function, errType: Function, msg?: string): void; 347 | Throw(fn: Function, errType: Function, regExp: RegExp): void; 348 | 349 | doesNotThrow(fn: Function, msg?: string): void; 350 | doesNotThrow(fn: Function, regExp: RegExp): void; 351 | doesNotThrow(fn: Function, errType: Function, msg?: string): void; 352 | doesNotThrow(fn: Function, errType: Function, regExp: RegExp): void; 353 | 354 | operator(val: any, operator: string, val2: any, msg?: string): void; 355 | closeTo(act: number, exp: number, delta: number, msg?: string): void; 356 | approximately(act: number, exp: number, delta: number, msg?: string): void; 357 | 358 | sameMembers(set1: any[], set2: any[], msg?: string): void; 359 | sameDeepMembers(set1: any[], set2: any[], msg?: string): void; 360 | includeMembers(superset: any[], subset: any[], msg?: string): void; 361 | 362 | ifError(val: any, msg?: string): void; 363 | 364 | isExtensible(obj: {}, msg?: string): void; 365 | extensible(obj: {}, msg?: string): void; 366 | isNotExtensible(obj: {}, msg?: string): void; 367 | notExtensible(obj: {}, msg?: string): void; 368 | 369 | isSealed(obj: {}, msg?: string): void; 370 | sealed(obj: {}, msg?: string): void; 371 | isNotSealed(obj: {}, msg?: string): void; 372 | notSealed(obj: {}, msg?: string): void; 373 | 374 | isFrozen(obj: Object, msg?: string): void; 375 | frozen(obj: Object, msg?: string): void; 376 | isNotFrozen(obj: Object, msg?: string): void; 377 | notFrozen(obj: Object, msg?: string): void; 378 | 379 | oneOf(inList: any, list: any[], msg?: string): void; 380 | } 381 | 382 | export interface Config { 383 | includeStack: boolean; 384 | } 385 | 386 | export class AssertionError { 387 | constructor(message: string, _props?: any, ssf?: Function); 388 | name: string; 389 | message: string; 390 | showDiff: boolean; 391 | stack: string; 392 | } 393 | } 394 | 395 | declare var chai: Chai.ChaiStatic; 396 | 397 | declare module "chai" { 398 | export = chai; 399 | } 400 | 401 | interface Object { 402 | should: Chai.Assertion; 403 | } --------------------------------------------------------------------------------