@homeostate/crdt-automerge 0.1.1 → 0.1.3
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 +27 -0
- package/README.md +0 -3
- package/dist/patching.js +1 -1
- package/dist/patching.js.map +1 -1
- package/package.json +6 -3
- package/src/automerge-backend.ts +37 -0
- package/src/handle.ts +51 -0
- package/src/index.ts +3 -0
- package/src/patching.ts +106 -0
- package/src/snapshot.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @homeostate/crdt-automerge
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 33c7b14: Mark the package side-effect free, so bundlers can drop modules that are not imported.
|
|
8
|
+
- fb76f77: Preserve complete Unicode code points when diffing and applying string edits. Deleting or
|
|
9
|
+
replacing supplementary characters such as emoji no longer corrupts text, deletes adjacent
|
|
10
|
+
characters, or throws in the supplied CRDT backends.
|
|
11
|
+
|
|
12
|
+
String `Change` offsets remain UTF-16 code units. A string deletion can now carry a numeric
|
|
13
|
+
UTF-16 deletion length in its third tuple entry; `undefined` still means one unit. Custom
|
|
14
|
+
backends consuming `getChanges` must honor this length in one operation rather than delete
|
|
15
|
+
one code unit per step. Array and object deletion semantics are unchanged.
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [a643bbe]
|
|
18
|
+
- Updated dependencies [0bbf653]
|
|
19
|
+
- Updated dependencies [fb76f77]
|
|
20
|
+
- @homeostate/core@0.2.0
|
|
21
|
+
|
|
22
|
+
## 0.1.2
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- 30abf27: Remove draft release warnings, update docs links
|
|
27
|
+
- Updated dependencies [30abf27]
|
|
28
|
+
- @homeostate/core@0.1.2
|
|
29
|
+
|
|
3
30
|
## 0.1.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# @homeostate/crdt-automerge
|
|
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
|
[Automerge](https://automerge.org) backend for
|
|
7
4
|
[`@homeostate/core`](https://github.com/mixedrays/homeostate/tree/main/packages/core).
|
|
8
5
|
It maps the synced state onto one key of an Automerge document (nested objects become
|
package/dist/patching.js
CHANGED
|
@@ -65,6 +65,6 @@ const applyToText = (doc, path, type, index, value) => {
|
|
|
65
65
|
if (type === ChangeType.INSERT)
|
|
66
66
|
A.splice(doc, path, index, 0, value);
|
|
67
67
|
else if (type === ChangeType.DELETE)
|
|
68
|
-
A.splice(doc, path, index, 1);
|
|
68
|
+
A.splice(doc, path, index, typeof value === "number" ? value : 1);
|
|
69
69
|
};
|
|
70
70
|
//# sourceMappingURL=patching.js.map
|
package/dist/patching.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patching.js","sourceRoot":"","sources":["../src/patching.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"patching.js","sourceRoot":"","sources":["../src/patching.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeostate/crdt-automerge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Automerge 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-automerge"
|
|
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
|
},
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
"default": "./dist/index.js"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
+
"sideEffects": false,
|
|
31
32
|
"files": [
|
|
32
33
|
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"!src/**/__tests__",
|
|
33
36
|
"README.md",
|
|
34
37
|
"LICENSE",
|
|
35
38
|
"THIRD_PARTY_NOTICES.md",
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
"access": "public"
|
|
40
43
|
},
|
|
41
44
|
"dependencies": {
|
|
42
|
-
"@homeostate/core": "^0.
|
|
45
|
+
"@homeostate/core": "^0.2.0"
|
|
43
46
|
},
|
|
44
47
|
"peerDependencies": {
|
|
45
48
|
"@automerge/automerge": "^3.0.0"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CrdtBackend, Unsubscribe } from "@homeostate/core";
|
|
2
|
+
import type { AutomergeHandle } from "./handle.js";
|
|
3
|
+
import { applyChanges, diff, toJson, type Container } from "./patching.js";
|
|
4
|
+
import { createSnapshot } from "./snapshot.js";
|
|
5
|
+
|
|
6
|
+
export const createAutomergeBackend = <T extends Container>(
|
|
7
|
+
handle: AutomergeHandle<T>,
|
|
8
|
+
name: string,
|
|
9
|
+
): CrdtBackend => {
|
|
10
|
+
const snapshot = createSnapshot();
|
|
11
|
+
let writing = false;
|
|
12
|
+
|
|
13
|
+
const current = (): unknown => (handle.doc() as Container)[name];
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
read: () => snapshot(current()) ?? {},
|
|
17
|
+
|
|
18
|
+
write: (next) => {
|
|
19
|
+
const changes = diff(current(), next);
|
|
20
|
+
if (changes?.length === 0) return;
|
|
21
|
+
writing = true;
|
|
22
|
+
try {
|
|
23
|
+
handle.change((doc) => {
|
|
24
|
+
if (changes === null) (doc as Container)[name] = toJson(next);
|
|
25
|
+
else applyChanges(doc as Container, [name], changes);
|
|
26
|
+
});
|
|
27
|
+
} finally {
|
|
28
|
+
writing = false;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
subscribe: (onRemoteChange): Unsubscribe =>
|
|
33
|
+
handle.subscribe(() => {
|
|
34
|
+
if (!writing) onRemoteChange();
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
};
|
package/src/handle.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as A from "@automerge/automerge";
|
|
2
|
+
import type { Unsubscribe } from "@homeostate/core";
|
|
3
|
+
|
|
4
|
+
export interface AutomergeHandleEvent<T> {
|
|
5
|
+
doc: A.Doc<T>;
|
|
6
|
+
local: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AutomergeHandle<T = Record<string, unknown>> {
|
|
10
|
+
doc: () => A.Doc<T>;
|
|
11
|
+
change: (fn: A.ChangeFn<T>) => void;
|
|
12
|
+
update: (fn: (doc: A.Doc<T>) => A.Doc<T>) => void;
|
|
13
|
+
subscribe: (
|
|
14
|
+
listener: (event: AutomergeHandleEvent<T>) => void,
|
|
15
|
+
) => Unsubscribe;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sameHeads = (a: A.Heads, b: A.Heads): boolean =>
|
|
19
|
+
a.length === b.length && a.every((hash, i) => hash === b[i]);
|
|
20
|
+
|
|
21
|
+
export const createAutomergeHandle = <T = Record<string, unknown>>(
|
|
22
|
+
initial: A.Doc<T> = A.init<T>(),
|
|
23
|
+
): AutomergeHandle<T> => {
|
|
24
|
+
let current = initial;
|
|
25
|
+
const listeners = new Set<(event: AutomergeHandleEvent<T>) => void>();
|
|
26
|
+
|
|
27
|
+
const replace = (
|
|
28
|
+
operation: (doc: A.Doc<T>) => A.Doc<T>,
|
|
29
|
+
local: boolean,
|
|
30
|
+
): void => {
|
|
31
|
+
const before = A.getHeads(current);
|
|
32
|
+
const next = operation(current);
|
|
33
|
+
if (next === current) return;
|
|
34
|
+
current = next;
|
|
35
|
+
if (sameHeads(before, A.getHeads(next))) return;
|
|
36
|
+
const event = { doc: next, local };
|
|
37
|
+
listeners.forEach((listener) => listener(event));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
doc: () => current,
|
|
42
|
+
change: (fn) => replace((doc) => A.change(doc, fn), true),
|
|
43
|
+
update: (fn) => replace(fn, false),
|
|
44
|
+
subscribe: (listener): Unsubscribe => {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
return () => {
|
|
47
|
+
listeners.delete(listener);
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
package/src/index.ts
ADDED
package/src/patching.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as A from "@automerge/automerge";
|
|
2
|
+
import { ChangeType, getChanges, type Change } from "@homeostate/core";
|
|
3
|
+
|
|
4
|
+
export type Container = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
const isRecord = (value: unknown): value is Container =>
|
|
7
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
|
|
9
|
+
export const toJson = (value: unknown): unknown => {
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map((item) => (item === undefined ? null : toJson(item)));
|
|
12
|
+
if (!isRecord(value)) return value;
|
|
13
|
+
const result: Container = {};
|
|
14
|
+
for (const [key, item] of Object.entries(value))
|
|
15
|
+
if (item !== undefined) result[key] = toJson(item);
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const diff = (current: unknown, next: unknown): Change[] | null => {
|
|
20
|
+
if (!isRecord(next)) return [];
|
|
21
|
+
return isRecord(current) ? getChanges(current, next) : null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const applyChanges = (
|
|
25
|
+
doc: Container,
|
|
26
|
+
path: A.Prop[],
|
|
27
|
+
changes: Change[],
|
|
28
|
+
): void => {
|
|
29
|
+
const container = path.reduce<unknown>(
|
|
30
|
+
(value, prop) => (value as Container)[prop],
|
|
31
|
+
doc,
|
|
32
|
+
);
|
|
33
|
+
for (const [type, key, value] of changes) {
|
|
34
|
+
if (typeof container === "string")
|
|
35
|
+
applyToText(doc, path, type, key as number, value);
|
|
36
|
+
else if (Array.isArray(container))
|
|
37
|
+
applyToList(doc, path, container, type, key as number, value);
|
|
38
|
+
else
|
|
39
|
+
applyToMap(doc, path, container as Container, type, key as string, value);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const applyToMap = (
|
|
44
|
+
doc: Container,
|
|
45
|
+
path: A.Prop[],
|
|
46
|
+
map: Container,
|
|
47
|
+
type: ChangeType,
|
|
48
|
+
key: string,
|
|
49
|
+
value: unknown,
|
|
50
|
+
): void => {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case ChangeType.INSERT:
|
|
53
|
+
case ChangeType.UPDATE:
|
|
54
|
+
if (value !== undefined) map[key] = toJson(value);
|
|
55
|
+
else if (key in map) delete map[key];
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case ChangeType.DELETE:
|
|
59
|
+
delete map[key];
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case ChangeType.PENDING:
|
|
63
|
+
applyChanges(doc, [...path, key], value as Change[]);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const applyToList = (
|
|
69
|
+
doc: Container,
|
|
70
|
+
path: A.Prop[],
|
|
71
|
+
list: unknown[],
|
|
72
|
+
type: ChangeType,
|
|
73
|
+
index: number,
|
|
74
|
+
value: unknown,
|
|
75
|
+
): void => {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case ChangeType.INSERT:
|
|
78
|
+
A.insertAt(list, index, toJson(value));
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case ChangeType.UPDATE:
|
|
82
|
+
list[index] = toJson(value);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case ChangeType.DELETE:
|
|
86
|
+
A.deleteAt(list, index, 1);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case ChangeType.PENDING:
|
|
90
|
+
applyChanges(doc, [...path, index], value as Change[]);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const applyToText = (
|
|
96
|
+
doc: Container,
|
|
97
|
+
path: A.Prop[],
|
|
98
|
+
type: ChangeType,
|
|
99
|
+
index: number,
|
|
100
|
+
value: unknown,
|
|
101
|
+
): void => {
|
|
102
|
+
if (type === ChangeType.INSERT)
|
|
103
|
+
A.splice(doc, path, index, 0, value as string);
|
|
104
|
+
else if (type === ChangeType.DELETE)
|
|
105
|
+
A.splice(doc, path, index, typeof value === "number" ? value : 1);
|
|
106
|
+
};
|
package/src/snapshot.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const createSnapshot = (): ((value: unknown) => unknown) => {
|
|
2
|
+
const copies = new WeakMap<object, unknown>();
|
|
3
|
+
|
|
4
|
+
const copy = (value: unknown): unknown => {
|
|
5
|
+
if (value === null || typeof value !== "object") return value;
|
|
6
|
+
const cached = copies.get(value);
|
|
7
|
+
if (cached !== undefined) return cached;
|
|
8
|
+
const result = Array.isArray(value)
|
|
9
|
+
? value.map(copy)
|
|
10
|
+
: Object.fromEntries(
|
|
11
|
+
Object.entries(value).map(([key, item]) => [key, copy(item)]),
|
|
12
|
+
);
|
|
13
|
+
copies.set(value, result);
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return copy;
|
|
18
|
+
};
|