@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 +51 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +10 -0
- package/package.json +57 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|