@absolutejs/sync-loro 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/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @absolutejs/sync-loro
2
+
3
+ A [Loro](https://loro.dev)-backed collaborative-text CRDT for
4
+ [`@absolutejs/sync`](https://github.com/absolutejs/sync), behind the same
5
+ `CrdtText` / `TextCrdtAdapter` contract as the core's zero-dependency `rgaText`
6
+ and `@absolutejs/sync-yjs`.
7
+
8
+ `@absolutejs/sync/crdt` ships a first-party RGA text CRDT that's great for
9
+ offline-merge and moderate collaboration. Loro is a fast, Rust/wasm CRDT library;
10
+ this adapter lets you swap it in without touching your call sites.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ bun add @absolutejs/sync-loro loro-crdt
16
+ ```
17
+
18
+ `@absolutejs/sync` is a peer dependency. `loro-crdt` is a runtime dependency you
19
+ install alongside.
20
+
21
+ ## Use
22
+
23
+ ```ts
24
+ import { loroText } from '@absolutejs/sync-loro';
25
+ // ...instead of: import { rgaText } from '@absolutejs/sync/crdt';
26
+
27
+ // Server — declare the CRDT field with this backend
28
+ engine.registerCrdt('doc', { state: loroText });
29
+
30
+ // Client — same hook, just pass the backend's factory
31
+ const doc = useCollaborativeText({
32
+ collection: 'doc',
33
+ field: 'state',
34
+ id: 'shared',
35
+ url,
36
+ create: (replica) => createLoroText(replica)
37
+ });
38
+ ```
39
+
40
+ The serialized state is a base64 string of a Loro snapshot — JSON-safe for the
41
+ sync engine's change feed. `merge` is commutative/associative/idempotent.
42
+
43
+ ## The contract
44
+
45
+ Implements `TextCrdtAdapter<string>` from `@absolutejs/sync/crdt`: `create`,
46
+ `merge`, `empty`, `textOf`. The `replica` argument is accepted for contract
47
+ compatibility; Loro assigns a peer id internally.
48
+
49
+ ## License
50
+
51
+ CC BY-NC 4.0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A [Loro](https://loro.dev)-backed collaborative-text CRDT for `@absolutejs/sync`,
3
+ * behind the same `CrdtText` / `TextCrdtAdapter` contract as the first-party
4
+ * `rgaText` and `@absolutejs/sync-yjs`. Loro is a fast, Rust/wasm CRDT library;
5
+ * swapping `rgaText` for `loroText` needs no other change at the call site.
6
+ *
7
+ * The serialized state is a **base64 string** of a Loro snapshot, so it stays
8
+ * JSON-safe for the sync engine's change feed and row storage. Merges are
9
+ * commutative/associative/idempotent.
10
+ */
11
+ import type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';
12
+ /** Create a live Loro-backed collaborative-text doc for `replica`. */
13
+ export declare const createLoroText: (replica: string, initial?: string) => CrdtText<string>;
14
+ /**
15
+ * The Loro collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in for
16
+ * the first-party `rgaText`. The `replica` argument is accepted for contract
17
+ * compatibility; Loro assigns a peer id internally.
18
+ */
19
+ export declare const loroText: TextCrdtAdapter<string>;
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { LoroDoc } from "loro-crdt";
4
+ var TEXT_KEY = "text";
5
+ var toBase64 = (bytes) => {
6
+ let binary = "";
7
+ for (const byte of bytes) {
8
+ binary += String.fromCharCode(byte);
9
+ }
10
+ return btoa(binary);
11
+ };
12
+ var fromBase64 = (base64) => {
13
+ const binary = atob(base64);
14
+ const bytes = new Uint8Array(binary.length);
15
+ for (let index = 0;index < binary.length; index += 1) {
16
+ bytes[index] = binary.charCodeAt(index);
17
+ }
18
+ return bytes;
19
+ };
20
+ var snapshot = (doc) => toBase64(doc.export({ mode: "snapshot" }));
21
+ var docFrom = (state) => {
22
+ const doc = new LoroDoc;
23
+ if (state !== undefined && state.length > 0) {
24
+ doc.import(fromBase64(state));
25
+ }
26
+ return doc;
27
+ };
28
+ var reconcile = (text, next) => {
29
+ const current = text.toString();
30
+ if (current === next) {
31
+ return;
32
+ }
33
+ let prefix = 0;
34
+ const maxPrefix = Math.min(current.length, next.length);
35
+ while (prefix < maxPrefix && current[prefix] === next[prefix]) {
36
+ prefix += 1;
37
+ }
38
+ let suffix = 0;
39
+ while (suffix < maxPrefix - prefix && current[current.length - 1 - suffix] === next[next.length - 1 - suffix]) {
40
+ suffix += 1;
41
+ }
42
+ const removed = current.length - prefix - suffix;
43
+ const inserted = next.slice(prefix, next.length - suffix);
44
+ if (removed > 0) {
45
+ text.delete(prefix, removed);
46
+ }
47
+ if (inserted.length > 0) {
48
+ text.insert(prefix, inserted);
49
+ }
50
+ };
51
+ var createLoroText = (replica, initial) => {
52
+ const doc = docFrom(initial);
53
+ const text = doc.getText(TEXT_KEY);
54
+ return {
55
+ merge: (state) => {
56
+ if (state.length > 0) {
57
+ doc.import(fromBase64(state));
58
+ }
59
+ },
60
+ setText: (next) => {
61
+ reconcile(text, next);
62
+ doc.commit();
63
+ },
64
+ state: () => snapshot(doc),
65
+ text: () => text.toString()
66
+ };
67
+ };
68
+ var loroText = {
69
+ create: createLoroText,
70
+ empty: () => snapshot(new LoroDoc),
71
+ merge: (a, b) => {
72
+ const doc = docFrom(a);
73
+ if (b.length > 0) {
74
+ doc.import(fromBase64(b));
75
+ }
76
+ return snapshot(doc);
77
+ },
78
+ textOf: (state) => docFrom(state).getText(TEXT_KEY).toString()
79
+ };
80
+ export {
81
+ loroText,
82
+ createLoroText
83
+ };
84
+
85
+ //# debugId=F14EF622F649D70764756E2164756E21
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * A [Loro](https://loro.dev)-backed collaborative-text CRDT for `@absolutejs/sync`,\n * behind the same `CrdtText` / `TextCrdtAdapter` contract as the first-party\n * `rgaText` and `@absolutejs/sync-yjs`. Loro is a fast, Rust/wasm CRDT library;\n * swapping `rgaText` for `loroText` needs no other change at the call site.\n *\n * The serialized state is a **base64 string** of a Loro snapshot, so it stays\n * JSON-safe for the sync engine's change feed and row storage. Merges are\n * commutative/associative/idempotent.\n */\nimport type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';\nimport { LoroDoc } from 'loro-crdt';\n\nconst TEXT_KEY = 'text';\n\nconst toBase64 = (bytes: Uint8Array): string => {\n\tlet binary = '';\n\tfor (const byte of bytes) {\n\t\tbinary += String.fromCharCode(byte);\n\t}\n\n\treturn btoa(binary);\n};\n\nconst fromBase64 = (base64: string): Uint8Array => {\n\tconst binary = atob(base64);\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let index = 0; index < binary.length; index += 1) {\n\t\tbytes[index] = binary.charCodeAt(index);\n\t}\n\n\treturn bytes;\n};\n\nconst snapshot = (doc: LoroDoc): string =>\n\ttoBase64(doc.export({ mode: 'snapshot' }));\n\nconst docFrom = (state?: string): LoroDoc => {\n\tconst doc = new LoroDoc();\n\tif (state !== undefined && state.length > 0) {\n\t\tdoc.import(fromBase64(state));\n\t}\n\n\treturn doc;\n};\n\n// Reconcile `text` to `next` by editing only the changed middle.\nconst reconcile = (text: ReturnType<LoroDoc['getText']>, next: string) => {\n\tconst current = text.toString();\n\tif (current === next) {\n\t\treturn;\n\t}\n\tlet prefix = 0;\n\tconst maxPrefix = Math.min(current.length, next.length);\n\twhile (prefix < maxPrefix && current[prefix] === next[prefix]) {\n\t\tprefix += 1;\n\t}\n\tlet suffix = 0;\n\twhile (\n\t\tsuffix < maxPrefix - prefix &&\n\t\tcurrent[current.length - 1 - suffix] === next[next.length - 1 - suffix]\n\t) {\n\t\tsuffix += 1;\n\t}\n\tconst removed = current.length - prefix - suffix;\n\tconst inserted = next.slice(prefix, next.length - suffix);\n\tif (removed > 0) {\n\t\ttext.delete(prefix, removed);\n\t}\n\tif (inserted.length > 0) {\n\t\ttext.insert(prefix, inserted);\n\t}\n};\n\n/** Create a live Loro-backed collaborative-text doc for `replica`. */\nexport const createLoroText = (\n\treplica: string,\n\tinitial?: string\n): CrdtText<string> => {\n\tconst doc = docFrom(initial);\n\tconst text = doc.getText(TEXT_KEY);\n\n\treturn {\n\t\tmerge: (state) => {\n\t\t\tif (state.length > 0) {\n\t\t\t\tdoc.import(fromBase64(state));\n\t\t\t}\n\t\t},\n\t\tsetText: (next) => {\n\t\t\treconcile(text, next);\n\t\t\tdoc.commit();\n\t\t},\n\t\tstate: () => snapshot(doc),\n\t\ttext: () => text.toString()\n\t};\n};\n\n/**\n * The Loro collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in for\n * the first-party `rgaText`. The `replica` argument is accepted for contract\n * compatibility; Loro assigns a peer id internally.\n */\nexport const loroText: TextCrdtAdapter<string> = {\n\tcreate: createLoroText,\n\tempty: () => snapshot(new LoroDoc()),\n\tmerge: (a, b) => {\n\t\tconst doc = docFrom(a);\n\t\tif (b.length > 0) {\n\t\t\tdoc.import(fromBase64(b));\n\t\t}\n\n\t\treturn snapshot(doc);\n\t},\n\ttextOf: (state) => docFrom(state).getText(TEXT_KEY).toString()\n};\n"
6
+ ],
7
+ "mappings": ";;AAWA;AAEA,IAAM,WAAW;AAEjB,IAAM,WAAW,CAAC,UAA8B;AAAA,EAC/C,IAAI,SAAS;AAAA,EACb,WAAW,QAAQ,OAAO;AAAA,IACzB,UAAU,OAAO,aAAa,IAAI;AAAA,EACnC;AAAA,EAEA,OAAO,KAAK,MAAM;AAAA;AAGnB,IAAM,aAAa,CAAC,WAA+B;AAAA,EAClD,MAAM,SAAS,KAAK,MAAM;AAAA,EAC1B,MAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAAA,EAC1C,SAAS,QAAQ,EAAG,QAAQ,OAAO,QAAQ,SAAS,GAAG;AAAA,IACtD,MAAM,SAAS,OAAO,WAAW,KAAK;AAAA,EACvC;AAAA,EAEA,OAAO;AAAA;AAGR,IAAM,WAAW,CAAC,QACjB,SAAS,IAAI,OAAO,EAAE,MAAM,WAAW,CAAC,CAAC;AAE1C,IAAM,UAAU,CAAC,UAA4B;AAAA,EAC5C,MAAM,MAAM,IAAI;AAAA,EAChB,IAAI,UAAU,aAAa,MAAM,SAAS,GAAG;AAAA,IAC5C,IAAI,OAAO,WAAW,KAAK,CAAC;AAAA,EAC7B;AAAA,EAEA,OAAO;AAAA;AAIR,IAAM,YAAY,CAAC,MAAsC,SAAiB;AAAA,EACzE,MAAM,UAAU,KAAK,SAAS;AAAA,EAC9B,IAAI,YAAY,MAAM;AAAA,IACrB;AAAA,EACD;AAAA,EACA,IAAI,SAAS;AAAA,EACb,MAAM,YAAY,KAAK,IAAI,QAAQ,QAAQ,KAAK,MAAM;AAAA,EACtD,OAAO,SAAS,aAAa,QAAQ,YAAY,KAAK,SAAS;AAAA,IAC9D,UAAU;AAAA,EACX;AAAA,EACA,IAAI,SAAS;AAAA,EACb,OACC,SAAS,YAAY,UACrB,QAAQ,QAAQ,SAAS,IAAI,YAAY,KAAK,KAAK,SAAS,IAAI,SAC/D;AAAA,IACD,UAAU;AAAA,EACX;AAAA,EACA,MAAM,UAAU,QAAQ,SAAS,SAAS;AAAA,EAC1C,MAAM,WAAW,KAAK,MAAM,QAAQ,KAAK,SAAS,MAAM;AAAA,EACxD,IAAI,UAAU,GAAG;AAAA,IAChB,KAAK,OAAO,QAAQ,OAAO;AAAA,EAC5B;AAAA,EACA,IAAI,SAAS,SAAS,GAAG;AAAA,IACxB,KAAK,OAAO,QAAQ,QAAQ;AAAA,EAC7B;AAAA;AAIM,IAAM,iBAAiB,CAC7B,SACA,YACsB;AAAA,EACtB,MAAM,MAAM,QAAQ,OAAO;AAAA,EAC3B,MAAM,OAAO,IAAI,QAAQ,QAAQ;AAAA,EAEjC,OAAO;AAAA,IACN,OAAO,CAAC,UAAU;AAAA,MACjB,IAAI,MAAM,SAAS,GAAG;AAAA,QACrB,IAAI,OAAO,WAAW,KAAK,CAAC;AAAA,MAC7B;AAAA;AAAA,IAED,SAAS,CAAC,SAAS;AAAA,MAClB,UAAU,MAAM,IAAI;AAAA,MACpB,IAAI,OAAO;AAAA;AAAA,IAEZ,OAAO,MAAM,SAAS,GAAG;AAAA,IACzB,MAAM,MAAM,KAAK,SAAS;AAAA,EAC3B;AAAA;AAQM,IAAM,WAAoC;AAAA,EAChD,QAAQ;AAAA,EACR,OAAO,MAAM,SAAS,IAAI,OAAS;AAAA,EACnC,OAAO,CAAC,GAAG,MAAM;AAAA,IAChB,MAAM,MAAM,QAAQ,CAAC;AAAA,IACrB,IAAI,EAAE,SAAS,GAAG;AAAA,MACjB,IAAI,OAAO,WAAW,CAAC,CAAC;AAAA,IACzB;AAAA,IAEA,OAAO,SAAS,GAAG;AAAA;AAAA,EAEpB,QAAQ,CAAC,UAAU,QAAQ,KAAK,EAAE,QAAQ,QAAQ,EAAE,SAAS;AAC9D;",
8
+ "debugId": "F14EF622F649D70764756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@absolutejs/sync-loro",
3
+ "version": "0.0.1",
4
+ "description": "Loro-backed collaborative-text CRDT adapter for @absolutejs/sync",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/sync-adapters.git",
8
+ "directory": "loro"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "type": "module",
14
+ "license": "CC BY-NC 4.0",
15
+ "author": "Alex Kahn",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "absolutejs",
21
+ "sync",
22
+ "crdt",
23
+ "loro",
24
+ "collaborative",
25
+ "multiplayer"
26
+ ],
27
+ "scripts": {
28
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync --external loro-crdt && tsc --project tsconfig.build.json",
29
+ "test": "bun test",
30
+ "typecheck": "tsc --noEmit",
31
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
32
+ "release": "bun run format && bun run build && bun publish"
33
+ },
34
+ "dependencies": {
35
+ "loro-crdt": "^1.12.2"
36
+ },
37
+ "peerDependencies": {
38
+ "@absolutejs/sync": ">= 0.10.0"
39
+ },
40
+ "devDependencies": {
41
+ "@absolutejs/sync": "^0.15.0",
42
+ "@types/bun": "1.3.14",
43
+ "prettier": "3.5.3",
44
+ "typescript": "5.8.3"
45
+ },
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist/index.d.ts",
49
+ "import": "./dist/index.js",
50
+ "default": "./dist/index.js"
51
+ }
52
+ },
53
+ "files": [
54
+ "dist",
55
+ "README.md"
56
+ ]
57
+ }