@absolutejs/sync-automerge 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 +20 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +10 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @absolutejs/sync-automerge
|
|
2
|
+
|
|
3
|
+
An [Automerge](https://automerge.org)-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. Automerge is a mature, battle-tested
|
|
10
|
+
CRDT library; this adapter lets you swap it in without touching your call sites.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @absolutejs/sync-automerge @automerge/automerge
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@absolutejs/sync` is a peer dependency. `@automerge/automerge` is a runtime
|
|
19
|
+
dependency you install alongside.
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { automergeText } from '@absolutejs/sync-automerge';
|
|
25
|
+
// ...instead of: import { rgaText } from '@absolutejs/sync/crdt';
|
|
26
|
+
|
|
27
|
+
// Server — declare the CRDT field with this backend
|
|
28
|
+
engine.registerCrdt('doc', { state: automergeText });
|
|
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) => createAutomergeText(replica)
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The serialized state is a base64 string of an Automerge document — JSON-safe for
|
|
41
|
+
the 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; Automerge manages actor identity internally.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
CC BY-NC 4.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Automerge-backed collaborative-text CRDT for `@absolutejs/sync`, behind the
|
|
3
|
+
* same `CrdtText` / `TextCrdtAdapter` contract as the first-party `rgaText` and
|
|
4
|
+
* `@absolutejs/sync-yjs`. Automerge is a mature, battle-tested CRDT library;
|
|
5
|
+
* swapping `rgaText` for `automergeText` needs no other change at the call site.
|
|
6
|
+
*
|
|
7
|
+
* The serialized state is a **base64 string** of an Automerge document, so it
|
|
8
|
+
* stays JSON-safe for the sync engine's change feed and row storage. Merges are
|
|
9
|
+
* commutative/associative/idempotent, so a merge-on-write mutation combines
|
|
10
|
+
* concurrent edits without clobbering.
|
|
11
|
+
*/
|
|
12
|
+
import type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';
|
|
13
|
+
/** Create a live Automerge-backed collaborative-text doc for `replica`. */
|
|
14
|
+
export declare const createAutomergeText: (replica: string, initial?: string) => CrdtText<string>;
|
|
15
|
+
/**
|
|
16
|
+
* The Automerge collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in
|
|
17
|
+
* for the first-party `rgaText`. The `replica` argument is accepted for contract
|
|
18
|
+
* compatibility; Automerge manages actor identity internally.
|
|
19
|
+
*/
|
|
20
|
+
export declare const automergeText: TextCrdtAdapter<string>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import * as Automerge from "@automerge/automerge";
|
|
4
|
+
var toBase64 = (bytes) => {
|
|
5
|
+
let binary = "";
|
|
6
|
+
for (const byte of bytes) {
|
|
7
|
+
binary += String.fromCharCode(byte);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
};
|
|
11
|
+
var fromBase64 = (base64) => {
|
|
12
|
+
const binary = atob(base64);
|
|
13
|
+
const bytes = new Uint8Array(binary.length);
|
|
14
|
+
for (let index = 0;index < binary.length; index += 1) {
|
|
15
|
+
bytes[index] = binary.charCodeAt(index);
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
};
|
|
19
|
+
var emptyDoc = () => Automerge.from({ text: "" });
|
|
20
|
+
var encode = (doc) => toBase64(Automerge.save(doc));
|
|
21
|
+
var decode = (state) => Automerge.load(fromBase64(state));
|
|
22
|
+
var load2 = (state) => state.length > 0 ? decode(state) : emptyDoc();
|
|
23
|
+
var reconcile = (doc, next) => {
|
|
24
|
+
const current = doc.text;
|
|
25
|
+
if (current === next) {
|
|
26
|
+
return doc;
|
|
27
|
+
}
|
|
28
|
+
let prefix = 0;
|
|
29
|
+
const maxPrefix = Math.min(current.length, next.length);
|
|
30
|
+
while (prefix < maxPrefix && current[prefix] === next[prefix]) {
|
|
31
|
+
prefix += 1;
|
|
32
|
+
}
|
|
33
|
+
let suffix = 0;
|
|
34
|
+
while (suffix < maxPrefix - prefix && current[current.length - 1 - suffix] === next[next.length - 1 - suffix]) {
|
|
35
|
+
suffix += 1;
|
|
36
|
+
}
|
|
37
|
+
const removed = current.length - prefix - suffix;
|
|
38
|
+
const inserted = next.slice(prefix, next.length - suffix);
|
|
39
|
+
return Automerge.change(doc, (draft) => {
|
|
40
|
+
Automerge.splice(draft, ["text"], prefix, removed, inserted);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
var createAutomergeText = (replica, initial) => {
|
|
44
|
+
let doc = initial !== undefined ? load2(initial) : emptyDoc();
|
|
45
|
+
return {
|
|
46
|
+
merge: (state) => {
|
|
47
|
+
if (state.length > 0) {
|
|
48
|
+
doc = Automerge.merge(doc, decode(state));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
setText: (next) => {
|
|
52
|
+
doc = reconcile(doc, next);
|
|
53
|
+
},
|
|
54
|
+
state: () => encode(doc),
|
|
55
|
+
text: () => doc.text
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
var automergeText = {
|
|
59
|
+
create: createAutomergeText,
|
|
60
|
+
empty: () => encode(emptyDoc()),
|
|
61
|
+
merge: (a, b) => {
|
|
62
|
+
const merged = b.length > 0 ? Automerge.merge(load2(a), decode(b)) : load2(a);
|
|
63
|
+
return encode(merged);
|
|
64
|
+
},
|
|
65
|
+
textOf: (state) => load2(state).text
|
|
66
|
+
};
|
|
67
|
+
export {
|
|
68
|
+
createAutomergeText,
|
|
69
|
+
automergeText
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//# debugId=FB0D6126D29D984364756E2164756E21
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * An Automerge-backed collaborative-text CRDT for `@absolutejs/sync`, behind the\n * same `CrdtText` / `TextCrdtAdapter` contract as the first-party `rgaText` and\n * `@absolutejs/sync-yjs`. Automerge is a mature, battle-tested CRDT library;\n * swapping `rgaText` for `automergeText` needs no other change at the call site.\n *\n * The serialized state is a **base64 string** of an Automerge document, so it\n * stays JSON-safe for the sync engine's change feed and row storage. Merges are\n * commutative/associative/idempotent, so a merge-on-write mutation combines\n * concurrent edits without clobbering.\n */\nimport type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';\nimport * as Automerge from '@automerge/automerge';\n\ntype Doc = { text: string };\n\n// Isomorphic base64 (browser + Bun expose btoa/atob), so the adapter runs\n// unchanged on the client and the server.\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 emptyDoc = (): Automerge.Doc<Doc> => Automerge.from<Doc>({ text: '' });\nconst encode = (doc: Automerge.Doc<Doc>): string =>\n\ttoBase64(Automerge.save(doc));\nconst decode = (state: string): Automerge.Doc<Doc> =>\n\tAutomerge.load<Doc>(fromBase64(state));\nconst load = (state: string): Automerge.Doc<Doc> =>\n\tstate.length > 0 ? decode(state) : emptyDoc();\n\n// Reconcile `text` to `next` by editing only the changed middle (keep the common\n// prefix/suffix), so two replicas editing different spots merge.\nconst reconcile = (\n\tdoc: Automerge.Doc<Doc>,\n\tnext: string\n): Automerge.Doc<Doc> => {\n\tconst current = doc.text;\n\tif (current === next) {\n\t\treturn doc;\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\n\treturn Automerge.change(doc, (draft) => {\n\t\tAutomerge.splice(draft, ['text'], prefix, removed, inserted);\n\t});\n};\n\n/** Create a live Automerge-backed collaborative-text doc for `replica`. */\nexport const createAutomergeText = (\n\treplica: string,\n\tinitial?: string\n): CrdtText<string> => {\n\tlet doc = initial !== undefined ? load(initial) : emptyDoc();\n\n\treturn {\n\t\tmerge: (state) => {\n\t\t\tif (state.length > 0) {\n\t\t\t\tdoc = Automerge.merge(doc, decode(state));\n\t\t\t}\n\t\t},\n\t\tsetText: (next) => {\n\t\t\tdoc = reconcile(doc, next);\n\t\t},\n\t\tstate: () => encode(doc),\n\t\ttext: () => doc.text\n\t};\n};\n\n/**\n * The Automerge collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in\n * for the first-party `rgaText`. The `replica` argument is accepted for contract\n * compatibility; Automerge manages actor identity internally.\n */\nexport const automergeText: TextCrdtAdapter<string> = {\n\tcreate: createAutomergeText,\n\tempty: () => encode(emptyDoc()),\n\tmerge: (a, b) => {\n\t\tconst merged =\n\t\t\tb.length > 0 ? Automerge.merge(load(a), decode(b)) : load(a);\n\n\t\treturn encode(merged);\n\t},\n\ttextOf: (state) => load(state).text\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;AAYA;AAMA,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,MAAoC,eAAU,EAAE,MAAM,GAAG,CAAC;AAC3E,IAAM,SAAS,CAAC,QACf,SAAmB,eAAK,GAAG,CAAC;AAC7B,IAAM,SAAS,CAAC,UACL,eAAU,WAAW,KAAK,CAAC;AACtC,IAAM,QAAO,CAAC,UACb,MAAM,SAAS,IAAI,OAAO,KAAK,IAAI,SAAS;AAI7C,IAAM,YAAY,CACjB,KACA,SACwB;AAAA,EACxB,MAAM,UAAU,IAAI;AAAA,EACpB,IAAI,YAAY,MAAM;AAAA,IACrB,OAAO;AAAA,EACR;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,EAExD,OAAiB,iBAAO,KAAK,CAAC,UAAU;AAAA,IAC7B,iBAAO,OAAO,CAAC,MAAM,GAAG,QAAQ,SAAS,QAAQ;AAAA,GAC3D;AAAA;AAIK,IAAM,sBAAsB,CAClC,SACA,YACsB;AAAA,EACtB,IAAI,MAAM,YAAY,YAAY,MAAK,OAAO,IAAI,SAAS;AAAA,EAE3D,OAAO;AAAA,IACN,OAAO,CAAC,UAAU;AAAA,MACjB,IAAI,MAAM,SAAS,GAAG;AAAA,QACrB,MAAgB,gBAAM,KAAK,OAAO,KAAK,CAAC;AAAA,MACzC;AAAA;AAAA,IAED,SAAS,CAAC,SAAS;AAAA,MAClB,MAAM,UAAU,KAAK,IAAI;AAAA;AAAA,IAE1B,OAAO,MAAM,OAAO,GAAG;AAAA,IACvB,MAAM,MAAM,IAAI;AAAA,EACjB;AAAA;AAQM,IAAM,gBAAyC;AAAA,EACrD,QAAQ;AAAA,EACR,OAAO,MAAM,OAAO,SAAS,CAAC;AAAA,EAC9B,OAAO,CAAC,GAAG,MAAM;AAAA,IAChB,MAAM,SACL,EAAE,SAAS,IAAc,gBAAM,MAAK,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,MAAK,CAAC;AAAA,IAE5D,OAAO,OAAO,MAAM;AAAA;AAAA,EAErB,QAAQ,CAAC,UAAU,MAAK,KAAK,EAAE;AAChC;",
|
|
8
|
+
"debugId": "FB0D6126D29D984364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/sync-automerge",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Automerge-backed collaborative-text CRDT adapter for @absolutejs/sync",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/sync-adapters.git",
|
|
8
|
+
"directory": "automerge"
|
|
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
|
+
"automerge",
|
|
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 @automerge/automerge && 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
|
+
"@automerge/automerge": "^3.2.6"
|
|
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
|
+
}
|