@homeostate/crdt-loro 0.1.1 → 0.1.2
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/CHANGELOG.md +8 -0
- package/README.md +0 -3
- package/package.json +5 -3
- package/src/index.ts +1 -0
- package/src/loro-backend.ts +24 -0
- package/src/mapping.ts +46 -0
- package/src/patching.ts +90 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# @homeostate/crdt-loro
|
|
2
2
|
|
|
3
|
-
> **Draft / placeholder release.** `0.0.0` reserves the name while the API is still
|
|
4
|
-
> being designed. Nothing here is stable — do not depend on it yet.
|
|
5
|
-
|
|
6
3
|
[Loro](https://github.com/loro-dev/loro) backend for
|
|
7
4
|
[`@homeostate/core`](https://github.com/mixedrays/homeostate/tree/main/packages/core).
|
|
8
5
|
It maps the synced state onto a `LoroMap` (nested objects become `LoroMap`, arrays
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeostate/crdt-loro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Loro CRDT backend for @homeostate/core",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/mixedrays/homeostate.git",
|
|
10
10
|
"directory": "packages/crdt-loro"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://homeostate.pages.dev",
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/mixedrays/homeostate/issues"
|
|
15
15
|
},
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"!src/**/__tests__",
|
|
33
35
|
"README.md",
|
|
34
36
|
"LICENSE",
|
|
35
37
|
"THIRD_PARTY_NOTICES.md",
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"access": "public"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@homeostate/core": "^0.1.
|
|
44
|
+
"@homeostate/core": "^0.1.2"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
45
47
|
"loro-crdt": "^1.0.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLoroBackend } from "./loro-backend.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LoroDoc } from "loro-crdt";
|
|
2
|
+
import type { CrdtBackend, Unsubscribe } from "@homeostate/core";
|
|
3
|
+
import { patchContainer } from "./patching.js";
|
|
4
|
+
|
|
5
|
+
let instances = 0;
|
|
6
|
+
|
|
7
|
+
export const createLoroBackend = (doc: LoroDoc, name: string): CrdtBackend => {
|
|
8
|
+
const map = doc.getMap(name);
|
|
9
|
+
const origin = `homeostate:${name}#${instances++}`;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
read: () => map.toJSON(),
|
|
13
|
+
|
|
14
|
+
write: (next) => {
|
|
15
|
+
patchContainer(map, next);
|
|
16
|
+
doc.commit({ origin });
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
subscribe: (onRemoteChange): Unsubscribe =>
|
|
20
|
+
map.subscribe((event) => {
|
|
21
|
+
if (event.origin !== origin) onRemoteChange();
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
};
|
package/src/mapping.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LoroList, LoroMap, LoroText } from "loro-crdt";
|
|
2
|
+
|
|
3
|
+
export type SharedContainer = LoroMap | LoroList | LoroText;
|
|
4
|
+
|
|
5
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
6
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
|
|
8
|
+
export const setMapEntry = (
|
|
9
|
+
map: LoroMap,
|
|
10
|
+
key: string,
|
|
11
|
+
value: unknown,
|
|
12
|
+
): void => {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
fillList(map.setContainer(key, new LoroList()), value);
|
|
15
|
+
else if (typeof value === "string")
|
|
16
|
+
map.setContainer(key, new LoroText()).insert(0, value);
|
|
17
|
+
else if (isRecord(value))
|
|
18
|
+
fillMap(map.setContainer(key, new LoroMap()), value);
|
|
19
|
+
else map.set(key, value);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const insertListItem = (
|
|
23
|
+
list: LoroList,
|
|
24
|
+
index: number,
|
|
25
|
+
value: unknown,
|
|
26
|
+
): void => {
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
fillList(list.insertContainer(index, new LoroList()), value);
|
|
29
|
+
else if (typeof value === "string")
|
|
30
|
+
list.insertContainer(index, new LoroText()).insert(0, value);
|
|
31
|
+
else if (isRecord(value))
|
|
32
|
+
fillMap(list.insertContainer(index, new LoroMap()), value);
|
|
33
|
+
else list.insert(index, value);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const fillMap = (
|
|
37
|
+
map: LoroMap,
|
|
38
|
+
object: Record<string, unknown>,
|
|
39
|
+
): void => {
|
|
40
|
+
for (const [key, value] of Object.entries(object))
|
|
41
|
+
setMapEntry(map, key, value);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const fillList = (list: LoroList, array: unknown[]): void => {
|
|
45
|
+
array.forEach((value, index) => insertListItem(list, index, value));
|
|
46
|
+
};
|
package/src/patching.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { LoroList, LoroMap, LoroText } from "loro-crdt";
|
|
2
|
+
import {
|
|
3
|
+
ChangeType,
|
|
4
|
+
getChanges,
|
|
5
|
+
type Change,
|
|
6
|
+
type Diffable,
|
|
7
|
+
} from "@homeostate/core";
|
|
8
|
+
import {
|
|
9
|
+
insertListItem,
|
|
10
|
+
setMapEntry,
|
|
11
|
+
type SharedContainer,
|
|
12
|
+
} from "./mapping.js";
|
|
13
|
+
|
|
14
|
+
export const patchContainer = (
|
|
15
|
+
container: SharedContainer,
|
|
16
|
+
newState: unknown,
|
|
17
|
+
): void => {
|
|
18
|
+
applyChanges(
|
|
19
|
+
container,
|
|
20
|
+
getChanges(container.toJSON() as Diffable, newState as Diffable),
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const applyChanges = (container: SharedContainer, changes: Change[]): void => {
|
|
25
|
+
for (const [type, key, value] of changes) {
|
|
26
|
+
if (container instanceof LoroMap)
|
|
27
|
+
applyToMap(container, type, key as string, value);
|
|
28
|
+
else if (container instanceof LoroList)
|
|
29
|
+
applyToList(container, type, key as number, value);
|
|
30
|
+
else applyToText(container, type, key as number, value);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const applyToMap = (
|
|
35
|
+
map: LoroMap,
|
|
36
|
+
type: ChangeType,
|
|
37
|
+
key: string,
|
|
38
|
+
value: unknown,
|
|
39
|
+
): void => {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case ChangeType.INSERT:
|
|
42
|
+
case ChangeType.UPDATE:
|
|
43
|
+
setMapEntry(map, key, value);
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case ChangeType.DELETE:
|
|
47
|
+
map.delete(key);
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case ChangeType.PENDING:
|
|
51
|
+
applyChanges(map.get(key) as SharedContainer, value as Change[]);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const applyToList = (
|
|
57
|
+
list: LoroList,
|
|
58
|
+
type: ChangeType,
|
|
59
|
+
index: number,
|
|
60
|
+
value: unknown,
|
|
61
|
+
): void => {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case ChangeType.INSERT:
|
|
64
|
+
insertListItem(list, index, value);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case ChangeType.UPDATE:
|
|
68
|
+
list.delete(index, 1);
|
|
69
|
+
insertListItem(list, index, value);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case ChangeType.DELETE:
|
|
73
|
+
list.delete(index, 1);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case ChangeType.PENDING:
|
|
77
|
+
applyChanges(list.get(index) as SharedContainer, value as Change[]);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const applyToText = (
|
|
83
|
+
text: LoroText,
|
|
84
|
+
type: ChangeType,
|
|
85
|
+
index: number,
|
|
86
|
+
value: unknown,
|
|
87
|
+
): void => {
|
|
88
|
+
if (type === ChangeType.INSERT) text.insert(index, value as string);
|
|
89
|
+
else if (type === ChangeType.DELETE) text.delete(index, 1);
|
|
90
|
+
};
|