├── .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 | }
--------------------------------------------------------------------------------