@homeostate/crdt-loro 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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # @homeostate/crdt-loro
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-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/dist/patching.js CHANGED
@@ -49,6 +49,6 @@ const applyToText = (text, type, index, value) => {
49
49
  if (type === ChangeType.INSERT)
50
50
  text.insert(index, value);
51
51
  else if (type === ChangeType.DELETE)
52
- text.delete(index, 1);
52
+ text.delete(index, typeof value === "number" ? value : 1);
53
53
  };
54
54
  //# sourceMappingURL=patching.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"patching.js","sourceRoot":"","sources":["../src/patching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAY,MAAM,WAAW,CAAC;AACxD,OAAO,EACL,UAAU,EACV,UAAU,GAGX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,WAAW,GAEZ,MAAM,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,SAA0B,EAC1B,QAAiB,EACX,EAAE;IACR,YAAY,CACV,SAAS,EACT,UAAU,CAAC,SAAS,CAAC,MAAM,EAAc,EAAE,QAAoB,CAAC,CACjE,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,SAA0B,EAAE,OAAiB,EAAQ,EAAE;IAC3E,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACzC,IAAI,SAAS,YAAY,OAAO;YAC9B,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;aAC/C,IAAI,SAAS,YAAY,QAAQ;YACpC,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;;YAChD,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CACjB,GAAY,EACZ,IAAgB,EAChB,GAAW,EACX,KAAc,EACR,EAAE;IACR,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,MAAM,CAAC;QACvB,KAAK,UAAU,CAAC,MAAM;YACpB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7B,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChB,MAAM;QAER,KAAK,UAAU,CAAC,OAAO;YACrB,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAoB,EAAE,KAAiB,CAAC,CAAC;YACjE,MAAM;IACV,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAClB,IAAc,EACd,IAAgB,EAChB,KAAa,EACb,KAAc,EACR,EAAE;IACR,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,MAAM;YACpB,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACtB,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACtB,MAAM;QAER,KAAK,UAAU,CAAC,OAAO;YACrB,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAoB,EAAE,KAAiB,CAAC,CAAC;YACpE,MAAM;IACV,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAClB,IAAc,EACd,IAAgB,EAChB,KAAa,EACb,KAAc,EACR,EAAE;IACR,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,KAAe,CAAC,CAAC;SAC/D,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAC7D,CAAC,CAAC"}
1
+ {"version":3,"file":"patching.js","sourceRoot":"","sources":["../src/patching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAY,MAAM,WAAW,CAAC;AACxD,OAAO,EACL,UAAU,EACV,UAAU,GAGX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,WAAW,GAEZ,MAAM,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,SAA0B,EAC1B,QAAiB,EACX,EAAE;IACR,YAAY,CACV,SAAS,EACT,UAAU,CAAC,SAAS,CAAC,MAAM,EAAc,EAAE,QAAoB,CAAC,CACjE,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,SAA0B,EAAE,OAAiB,EAAQ,EAAE;IAC3E,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACzC,IAAI,SAAS,YAAY,OAAO;YAC9B,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;aAC/C,IAAI,SAAS,YAAY,QAAQ;YACpC,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;;YAChD,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,GAAa,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CACjB,GAAY,EACZ,IAAgB,EAChB,GAAW,EACX,KAAc,EACR,EAAE;IACR,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,MAAM,CAAC;QACvB,KAAK,UAAU,CAAC,MAAM;YACpB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7B,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChB,MAAM;QAER,KAAK,UAAU,CAAC,OAAO;YACrB,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAoB,EAAE,KAAiB,CAAC,CAAC;YACjE,MAAM;IACV,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAClB,IAAc,EACd,IAAgB,EAChB,KAAa,EACb,KAAc,EACR,EAAE;IACR,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,MAAM;YACpB,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACtB,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM;QAER,KAAK,UAAU,CAAC,MAAM;YACpB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACtB,MAAM;QAER,KAAK,UAAU,CAAC,OAAO;YACrB,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAoB,EAAE,KAAiB,CAAC,CAAC;YACpE,MAAM;IACV,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAClB,IAAc,EACd,IAAgB,EAChB,KAAa,EACb,KAAc,EACR,EAAE;IACR,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,KAAe,CAAC,CAAC;SAC/D,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QACjC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeostate/crdt-loro",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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://github.com/mixedrays/homeostate#readme",
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.1.1"
45
+ "@homeostate/core": "^0.2.0"
43
46
  },
44
47
  "peerDependencies": {
45
48
  "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
+ };
@@ -0,0 +1,91 @@
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)
90
+ text.delete(index, typeof value === "number" ? value : 1);
91
+ };