@cr_docs_t/dts 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/biome.jsonc +12 -0
- package/dist/CausalTree/CTNode.js +11 -0
- package/dist/CausalTree/CausalTree.js +19 -0
- package/dist/Fugue/FNode.js +11 -0
- package/dist/Fugue/FugueList.js +80 -0
- package/dist/Fugue/FugueTest.js +26 -0
- package/dist/Fugue/utils.js +47 -0
- package/dist/index.js +2 -0
- package/package.json +22 -0
- package/src/CausalTree/CTNode.ts +12 -0
- package/src/CausalTree/CausalTree.ts +21 -0
- package/src/Fugue/FNode.ts +11 -0
- package/src/Fugue/FugueList.ts +82 -0
- package/src/Fugue/FugueTest.ts +25 -0
- package/src/Fugue/utils.ts +75 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +13 -0
package/biome.jsonc
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class CasualTree {
|
|
4
|
+
/*
|
|
5
|
+
This is the fug crdt
|
|
6
|
+
It is a tree
|
|
7
|
+
It has a root
|
|
8
|
+
|
|
9
|
+
We can insert into the tree
|
|
10
|
+
Delete from the tree
|
|
11
|
+
Display the characters...
|
|
12
|
+
Maybe add a build method from a list of operations...
|
|
13
|
+
*/
|
|
14
|
+
root;
|
|
15
|
+
constructor(root) {
|
|
16
|
+
this.root = root;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.default = CasualTree;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const FNode_1 = __importDefault(require("./FNode"));
|
|
7
|
+
/**
|
|
8
|
+
* A Fugue List CRDT, with insert and delete operations
|
|
9
|
+
*/
|
|
10
|
+
class FugueList {
|
|
11
|
+
state = [];
|
|
12
|
+
totalOrder;
|
|
13
|
+
positionCounter = 0;
|
|
14
|
+
constructor(totalOrder) {
|
|
15
|
+
this.totalOrder = totalOrder;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Inserts new element with 'value' at 'index' in the list
|
|
19
|
+
* @param index - Index to insert 'value' at
|
|
20
|
+
* @param value - Value to insert
|
|
21
|
+
*/
|
|
22
|
+
insert(index, value) {
|
|
23
|
+
if (index >= this.state.length)
|
|
24
|
+
this.state.push([]);
|
|
25
|
+
let i = this.state.length - 1;
|
|
26
|
+
while (i > index) {
|
|
27
|
+
this.state[i] = this.state[i - 1];
|
|
28
|
+
i--;
|
|
29
|
+
}
|
|
30
|
+
const atIndex = this.state[index];
|
|
31
|
+
if (index > 0 && index < this.state.length - 1) {
|
|
32
|
+
const before = this.state[index - 1];
|
|
33
|
+
const after = this.state[index + 1];
|
|
34
|
+
if (atIndex.length == 0) {
|
|
35
|
+
atIndex.push(new FNode_1.default(this.totalOrder.createBetween(before[before.length - 1].position), value));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const a = atIndex[atIndex.length - 1];
|
|
39
|
+
atIndex.push(new FNode_1.default(this.totalOrder.createBetween(a.position, after[after.length - 1].position), value));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (atIndex.length == 0) {
|
|
44
|
+
atIndex.push(new FNode_1.default(this.totalOrder.createBetween(), value));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const a = atIndex[atIndex.length - 1];
|
|
48
|
+
atIndex.push(new FNode_1.default(this.totalOrder.createBetween(a.position), value));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Delete value in the list at index
|
|
54
|
+
* @param index - Index of the value to delete
|
|
55
|
+
*/
|
|
56
|
+
delete(index) {
|
|
57
|
+
let i = index;
|
|
58
|
+
while (i < this.state.length) {
|
|
59
|
+
this.state[i] = this.state[i + 1];
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
this.state.pop();
|
|
63
|
+
}
|
|
64
|
+
observe() {
|
|
65
|
+
let res = new String();
|
|
66
|
+
for (const idx of this.state) {
|
|
67
|
+
if (idx.length > 1) {
|
|
68
|
+
res += idx
|
|
69
|
+
.sort((a, b) => this.totalOrder.compare(a.position, b.position))
|
|
70
|
+
.map((node) => node?.value || "ð")
|
|
71
|
+
.join("");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
res += idx[0]?.value || "ð";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return res.toString();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.default = FugueList;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const FugueList_1 = __importDefault(require("./FugueList"));
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const test1 = new FugueList_1.default(new utils_1.StringTotalOrder((0, utils_1.randomString)(5)));
|
|
9
|
+
const test2 = new FugueList_1.default(new utils_1.StringTotalOrder((0, utils_1.randomString)(5)));
|
|
10
|
+
const word1 = "SHADOW WIZARD MONEY GANG";
|
|
11
|
+
const word2 = "BALLING";
|
|
12
|
+
// Randomly insert words into the list from different simulated users
|
|
13
|
+
async function simulateUser(list, word) {
|
|
14
|
+
for (let i = 0; i < word.length; i++) {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 150));
|
|
16
|
+
list.insert(i, word.charAt(i));
|
|
17
|
+
console.log(list.observe());
|
|
18
|
+
console.log(list.state);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
Promise.all([
|
|
22
|
+
simulateUser(test1, word1), //
|
|
23
|
+
simulateUser(test2, word2),
|
|
24
|
+
])
|
|
25
|
+
.then(() => console.log(`Done:\t${test1.observe()}\n\t${test2.observe()}`))
|
|
26
|
+
.catch(console.error);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StringTotalOrder = void 0;
|
|
4
|
+
exports.randomString = randomString;
|
|
5
|
+
class StringTotalOrder {
|
|
6
|
+
replicaID;
|
|
7
|
+
counter = 0;
|
|
8
|
+
compare(a, b) {
|
|
9
|
+
return a.localeCompare(b);
|
|
10
|
+
}
|
|
11
|
+
constructor(replicaID) {
|
|
12
|
+
this.replicaID = replicaID;
|
|
13
|
+
}
|
|
14
|
+
createBetween(a, b) {
|
|
15
|
+
// Create a wholly unique string using a causal dot, i.e. (replicaID, counter)
|
|
16
|
+
const uniqueStr = `${this.replicaID}${this.counter++}`;
|
|
17
|
+
// If node is the first ever position in the document
|
|
18
|
+
if (!a && !b) {
|
|
19
|
+
return uniqueStr + "R";
|
|
20
|
+
}
|
|
21
|
+
// If node is the first position at that index
|
|
22
|
+
if (!a) {
|
|
23
|
+
return b + uniqueStr + "R";
|
|
24
|
+
}
|
|
25
|
+
// If node is the last position at that index
|
|
26
|
+
if (!b) {
|
|
27
|
+
return a + uniqueStr + "R";
|
|
28
|
+
}
|
|
29
|
+
const isAPrefixOfB = b.substring(0, a.length).localeCompare(a);
|
|
30
|
+
// If a is not a prefix of b append a globally unique new string to a and return that +R
|
|
31
|
+
if (!isAPrefixOfB) {
|
|
32
|
+
return a + uniqueStr + "R";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// If a is a prefix of b replace the R at the end of b with L.
|
|
36
|
+
// Then append a globally unique string to it and return it +R.
|
|
37
|
+
return b.slice(0, -1) + "L" + uniqueStr + "R";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.StringTotalOrder = StringTotalOrder;
|
|
42
|
+
function randomString(length = 10) {
|
|
43
|
+
let res = new Array(length);
|
|
44
|
+
for (let i = 0; i < length; i++)
|
|
45
|
+
res[i] = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
46
|
+
return res.join("");
|
|
47
|
+
}
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cr_docs_t/dts",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"publish": "npm run build && npm publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": [
|
|
13
|
+
"Madiba Hudson-Quansah",
|
|
14
|
+
"Tanitoluwa Olamiji Adebayo"
|
|
15
|
+
],
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"packageManager": "pnpm@10.20.0",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^24.10.1",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import CTNode from "./CTNode";
|
|
2
|
+
|
|
3
|
+
class CasualTree {
|
|
4
|
+
/*
|
|
5
|
+
This is the fug crdt
|
|
6
|
+
It is a tree
|
|
7
|
+
It has a root
|
|
8
|
+
|
|
9
|
+
We can insert into the tree
|
|
10
|
+
Delete from the tree
|
|
11
|
+
Display the characters...
|
|
12
|
+
Maybe add a build method from a list of operations...
|
|
13
|
+
*/
|
|
14
|
+
root: CTNode;
|
|
15
|
+
|
|
16
|
+
constructor(root: CTNode) {
|
|
17
|
+
this.root = root;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default CasualTree;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import FNode from "./FNode";
|
|
2
|
+
import { StringTotalOrder, UniquelyDenseTotalOrder } from "./utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Fugue List CRDT, with insert and delete operations
|
|
6
|
+
*/
|
|
7
|
+
class FugueList<P> {
|
|
8
|
+
state: FNode<P>[][] = [];
|
|
9
|
+
totalOrder: UniquelyDenseTotalOrder<P>;
|
|
10
|
+
positionCounter = 0;
|
|
11
|
+
|
|
12
|
+
constructor(totalOrder: UniquelyDenseTotalOrder<P>) {
|
|
13
|
+
this.totalOrder = totalOrder;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inserts new element with 'value' at 'index' in the list
|
|
18
|
+
* @param index - Index to insert 'value' at
|
|
19
|
+
* @param value - Value to insert
|
|
20
|
+
*/
|
|
21
|
+
insert(index: number, value: string) {
|
|
22
|
+
if (index >= this.state.length) this.state.push([]);
|
|
23
|
+
|
|
24
|
+
let i = this.state.length - 1;
|
|
25
|
+
while (i > index) {
|
|
26
|
+
this.state[i] = this.state[i - 1];
|
|
27
|
+
i--;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const atIndex = this.state[index];
|
|
31
|
+
if (index > 0 && index < this.state.length - 1) {
|
|
32
|
+
const before = this.state[index - 1];
|
|
33
|
+
const after = this.state[index + 1];
|
|
34
|
+
if (atIndex.length == 0) {
|
|
35
|
+
atIndex.push(new FNode<P>(this.totalOrder.createBetween(before[before.length - 1].position), value));
|
|
36
|
+
} else {
|
|
37
|
+
const a = atIndex[atIndex.length - 1];
|
|
38
|
+
atIndex.push(
|
|
39
|
+
new FNode<P>(this.totalOrder.createBetween(a.position, after[after.length - 1].position), value),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
if (atIndex.length == 0) {
|
|
44
|
+
atIndex.push(new FNode<P>(this.totalOrder.createBetween(), value));
|
|
45
|
+
} else {
|
|
46
|
+
const a = atIndex[atIndex.length - 1];
|
|
47
|
+
atIndex.push(new FNode<P>(this.totalOrder.createBetween(a.position), value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Delete value in the list at index
|
|
54
|
+
* @param index - Index of the value to delete
|
|
55
|
+
*/
|
|
56
|
+
delete(index: number) {
|
|
57
|
+
let i = index;
|
|
58
|
+
while (i < this.state.length) {
|
|
59
|
+
this.state[i] = this.state[i + 1];
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
this.state.pop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
observe(): string {
|
|
66
|
+
let res = new String();
|
|
67
|
+
for (const idx of this.state) {
|
|
68
|
+
if (idx.length > 1) {
|
|
69
|
+
res += idx
|
|
70
|
+
.sort((a, b) => this.totalOrder.compare(a.position, b.position))
|
|
71
|
+
.map((node) => node?.value || "ð")
|
|
72
|
+
.join("");
|
|
73
|
+
} else {
|
|
74
|
+
res += idx[0]?.value || "ð";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return res.toString();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default FugueList;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import FugueList from "./FugueList";
|
|
2
|
+
import { randomString, StringTotalOrder } from "./utils";
|
|
3
|
+
|
|
4
|
+
const test1 = new FugueList(new StringTotalOrder(randomString(5)));
|
|
5
|
+
const test2 = new FugueList(new StringTotalOrder(randomString(5)));
|
|
6
|
+
|
|
7
|
+
const word1 = "SHADOW WIZARD MONEY GANG";
|
|
8
|
+
const word2 = "BALLING";
|
|
9
|
+
|
|
10
|
+
// Randomly insert words into the list from different simulated users
|
|
11
|
+
async function simulateUser(list: FugueList<string>, word: string) {
|
|
12
|
+
for (let i = 0; i < word.length; i++) {
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 150));
|
|
14
|
+
list.insert(i, word.charAt(i));
|
|
15
|
+
console.log(list.observe());
|
|
16
|
+
console.log(list.state);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Promise.all([
|
|
21
|
+
simulateUser(test1, word1), //
|
|
22
|
+
simulateUser(test2, word2),
|
|
23
|
+
])
|
|
24
|
+
.then(() => console.log(`Done:\t${test1.observe()}\n\t${test2.observe()}`))
|
|
25
|
+
.catch(console.error);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper interface for sorting and creating unique immutable positions,
|
|
3
|
+
* suitable for use in a List CRDT. Taken from mattweidner.com/2022/10/21/basic-list-crdt.html
|
|
4
|
+
*
|
|
5
|
+
* @type P The type of positions. Treated as immutable.
|
|
6
|
+
*/
|
|
7
|
+
export interface UniquelyDenseTotalOrder<P> {
|
|
8
|
+
/**
|
|
9
|
+
* Usual compare function for sorts: returns negative if a < b in
|
|
10
|
+
* their sort order, positive if a > b.
|
|
11
|
+
*/
|
|
12
|
+
compare(a: P, b: P): number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a globally unique new position c such that a < c < b.
|
|
16
|
+
*
|
|
17
|
+
* "Globally unique" means that the created position must be distinct
|
|
18
|
+
* from all other created positions, including ones created concurrently
|
|
19
|
+
* by other users.
|
|
20
|
+
*
|
|
21
|
+
* When a is undefined, it is treated as the start of the list, i.e.,
|
|
22
|
+
* this returns c such that c < b. Likewise, undefined b is treated
|
|
23
|
+
* as the end of the list.
|
|
24
|
+
*/
|
|
25
|
+
createBetween(a?: P, b?: P): P;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class StringTotalOrder implements UniquelyDenseTotalOrder<string> {
|
|
29
|
+
readonly replicaID: string;
|
|
30
|
+
private counter = 0;
|
|
31
|
+
|
|
32
|
+
compare(a: string, b: string): number {
|
|
33
|
+
return a.localeCompare(b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor(replicaID: string) {
|
|
37
|
+
this.replicaID = replicaID;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
createBetween(a?: string, b?: string): string {
|
|
41
|
+
// Create a wholly unique string using a causal dot, i.e. (replicaID, counter)
|
|
42
|
+
const uniqueStr = `${this.replicaID}${this.counter++}`;
|
|
43
|
+
|
|
44
|
+
// If node is the first ever position in the document
|
|
45
|
+
if (!a && !b) {
|
|
46
|
+
return uniqueStr + "R";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If node is the first position at that index
|
|
50
|
+
if (!a) {
|
|
51
|
+
return b + uniqueStr + "R";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If node is the last position at that index
|
|
55
|
+
if (!b) {
|
|
56
|
+
return a + uniqueStr + "R";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isAPrefixOfB = b.substring(0, a.length).localeCompare(a);
|
|
60
|
+
// If a is not a prefix of b append a globally unique new string to a and return that +R
|
|
61
|
+
if (!isAPrefixOfB) {
|
|
62
|
+
return a + uniqueStr + "R";
|
|
63
|
+
} else {
|
|
64
|
+
// If a is a prefix of b replace the R at the end of b with L.
|
|
65
|
+
// Then append a globally unique string to it and return it +R.
|
|
66
|
+
return b.slice(0, -1) + "L" + uniqueStr + "R";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function randomString(length: number = 10): string {
|
|
72
|
+
let res = new Array<string>(length);
|
|
73
|
+
for (let i = 0; i < length; i++) res[i] = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
74
|
+
return res.join("");
|
|
75
|
+
}
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
}
|
|
13
|
+
}
|