@absolutejs/sync-yjs 0.0.5 → 0.1.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 +2 -0
- package/dist/index.js +14 -18
- package/dist/index.js.map +3 -3
- package/dist/manifest.js +339 -347
- package/dist/manifest.js.map +15 -15
- package/package.json +5 -15
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ doc.setText(textarea.value); // reconciles via a minimal diff
|
|
|
31
31
|
socket.send(doc.state()); // broadcast the serialized (base64) state
|
|
32
32
|
doc.merge(incomingState); // fold a peer's state into local edits
|
|
33
33
|
doc.text(); // the merged visible text
|
|
34
|
+
const cursor = doc.anchorAt?.(textarea.selectionStart); // stable relative cursor
|
|
35
|
+
doc.indexOfAnchor?.(cursor ?? null); // follows concurrent inserts/deletes
|
|
34
36
|
|
|
35
37
|
// Server — merge-on-write so concurrent writes combine instead of clobbering
|
|
36
38
|
const next = yjsText.merge(storedState, incomingState);
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true,
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
|
|
17
2
|
// src/index.ts
|
|
18
3
|
import * as Y from "yjs";
|
|
19
4
|
var TEXT_KEY = "text";
|
|
@@ -81,6 +66,17 @@ var createYjsText = (replica, initial) => {
|
|
|
81
66
|
const text = doc.getText(TEXT_KEY);
|
|
82
67
|
let lastVector = Y.encodeStateVector(doc);
|
|
83
68
|
return {
|
|
69
|
+
anchorAt: (index) => toBase64(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, Math.max(0, Math.min(index, text.length))))),
|
|
70
|
+
indexOfAnchor: (anchor) => {
|
|
71
|
+
if (anchor === null)
|
|
72
|
+
return 0;
|
|
73
|
+
try {
|
|
74
|
+
const absolute = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(fromBase64(anchor)), doc);
|
|
75
|
+
return absolute?.type === text ? absolute.index : 0;
|
|
76
|
+
} catch {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
84
80
|
merge: (state) => {
|
|
85
81
|
apply(doc, state);
|
|
86
82
|
lastVector = Y.encodeStateVector(doc);
|
|
@@ -111,9 +107,9 @@ var yjsText = {
|
|
|
111
107
|
}
|
|
112
108
|
};
|
|
113
109
|
export {
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
createYjsText,
|
|
111
|
+
yjsText
|
|
116
112
|
};
|
|
117
113
|
|
|
118
|
-
//# debugId=
|
|
114
|
+
//# debugId=C99C343FC96084F064756E2164756E21
|
|
119
115
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * A Yjs-backed collaborative-text CRDT for `@absolutejs/sync`.\n *\n * `@absolutejs/sync` ships a zero-dependency RGA text CRDT (`rgaText`) that is\n * fine for offline-merge and moderate collaboration. Yjs is the community staple\n * for production-scale collaborative text — it solves the real footguns (tombstone\n * growth, efficient deltas, interleaving). This adapter wraps Yjs behind the exact\n * same `CrdtText` / `TextCrdtAdapter` contract, so swapping `rgaText` for `yjsText`\n * needs no other change at the call site.\n *\n * The serialized state is a **base64 string** of a Yjs update, so it stays\n * JSON-safe for transport over the sync engine's change feed and storage as a row\n * field. Merges are commutative/associative/idempotent (Yjs guarantees it), so a\n * merge-on-write mutation combines concurrent edits without clobbering.\n */\nimport type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';\nimport * as Y from 'yjs';\n\nconst TEXT_KEY = 'text';\n\n// Isomorphic base64 (browser + Bun both expose btoa/atob), so the adapter runs\n// unchanged on the client (live editing) and the server (merge-on-write).\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 encode = (doc: Y.Doc): string => toBase64(Y.encodeStateAsUpdate(doc));\n\nconst apply = (doc: Y.Doc, state: string) => {\n\tif (state.length > 0) {\n\t\tY.applyUpdate(doc, fromBase64(state));\n\t}\n};\n\n// A stable, deterministic Yjs clientID per replica id (Yjs needs distinct\n// clients to keep distinct identities; deriving it from `replica` also makes\n// merges reproducible instead of depending on a random per-instance id).\nconst clientIdFor = (replica: string): number => {\n\tlet hash = 0;\n\tfor (let index = 0; index < replica.length; index += 1) {\n\t\thash = (Math.imul(hash, 31) + replica.charCodeAt(index)) | 0;\n\t}\n\n\treturn hash >>> 0;\n};\n\nconst docFrom = (replica: string, initial?: string): Y.Doc => {\n\tconst doc = new Y.Doc();\n\tdoc.clientID = clientIdFor(replica);\n\tif (initial !== undefined) {\n\t\tapply(doc, initial);\n\t}\n\n\treturn doc;\n};\n\n/** Reconcile `text` to `next` by editing only the changed middle (keep the\n * common prefix/suffix), so two replicas editing different spots merge. */\nconst reconcile = (text: Y.Text, 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 Yjs-backed collaborative-text doc for `replica`. */\nexport const createYjsText = (\n\treplica: string,\n\tinitial?: string\n): CrdtText<string> => {\n\tconst doc = docFrom(replica, initial);\n\tconst text = doc.getText(TEXT_KEY);\n\t// State vector at the last sync point (a take or a merge). takeDelta encodes\n\t// every update since this vector and then advances it; merge advances it too,\n\t// so a remote op merged in isn't re-broadcast on the next take.\n\tlet lastVector = Y.encodeStateVector(doc);\n\n\treturn {\n\t\tmerge: (state) => {\n\t\t\tapply(doc, state);\n\t\t\tlastVector = Y.encodeStateVector(doc);\n\t\t},\n\t\tsetText: (next) => doc.transact(() => reconcile(text, next)),\n\t\tstate: () => encode(doc),\n\t\ttakeDelta: () => {\n\t\t\tconst update = Y.encodeStateAsUpdate(doc, lastVector);\n\t\t\tlastVector = Y.encodeStateVector(doc);\n\n\t\t\treturn toBase64(update);\n\t\t},\n\t\ttext: () => text.toString()\n\t};\n};\n\n/**\n * The Yjs collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in for\n * the first-party `rgaText`: `create` mints a live doc, `merge` combines two\n * persisted states server-side, `empty`/`textOf` are conveniences.\n */\nexport const yjsText: TextCrdtAdapter<string> = {\n\tcreate: createYjsText,\n\tempty: () => encode(new Y.Doc()),\n\tmerge: (a, b) => {\n\t\tconst doc = new Y.Doc();\n\t\tapply(doc, a);\n\t\tapply(doc, b);\n\n\t\treturn encode(doc);\n\t},\n\ttextOf: (state) => {\n\t\tconst doc = new Y.Doc();\n\t\tapply(doc, state);\n\n\t\treturn doc.getText(TEXT_KEY).toString();\n\t}\n};\n"
|
|
5
|
+
"/**\n * A Yjs-backed collaborative-text CRDT for `@absolutejs/sync`.\n *\n * `@absolutejs/sync` ships a zero-dependency RGA text CRDT (`rgaText`) that is\n * fine for offline-merge and moderate collaboration. Yjs is the community staple\n * for production-scale collaborative text — it solves the real footguns (tombstone\n * growth, efficient deltas, interleaving). This adapter wraps Yjs behind the exact\n * same `CrdtText` / `TextCrdtAdapter` contract, so swapping `rgaText` for `yjsText`\n * needs no other change at the call site.\n *\n * The serialized state is a **base64 string** of a Yjs update, so it stays\n * JSON-safe for transport over the sync engine's change feed and storage as a row\n * field. Merges are commutative/associative/idempotent (Yjs guarantees it), so a\n * merge-on-write mutation combines concurrent edits without clobbering.\n */\nimport type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';\nimport * as Y from 'yjs';\n\nconst TEXT_KEY = 'text';\n\n// Isomorphic base64 (browser + Bun both expose btoa/atob), so the adapter runs\n// unchanged on the client (live editing) and the server (merge-on-write).\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 encode = (doc: Y.Doc): string => toBase64(Y.encodeStateAsUpdate(doc));\n\nconst apply = (doc: Y.Doc, state: string) => {\n\tif (state.length > 0) {\n\t\tY.applyUpdate(doc, fromBase64(state));\n\t}\n};\n\n// A stable, deterministic Yjs clientID per replica id (Yjs needs distinct\n// clients to keep distinct identities; deriving it from `replica` also makes\n// merges reproducible instead of depending on a random per-instance id).\nconst clientIdFor = (replica: string): number => {\n\tlet hash = 0;\n\tfor (let index = 0; index < replica.length; index += 1) {\n\t\thash = (Math.imul(hash, 31) + replica.charCodeAt(index)) | 0;\n\t}\n\n\treturn hash >>> 0;\n};\n\nconst docFrom = (replica: string, initial?: string): Y.Doc => {\n\tconst doc = new Y.Doc();\n\tdoc.clientID = clientIdFor(replica);\n\tif (initial !== undefined) {\n\t\tapply(doc, initial);\n\t}\n\n\treturn doc;\n};\n\n/** Reconcile `text` to `next` by editing only the changed middle (keep the\n * common prefix/suffix), so two replicas editing different spots merge. */\nconst reconcile = (text: Y.Text, 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 Yjs-backed collaborative-text doc for `replica`. */\nexport const createYjsText = (\n\treplica: string,\n\tinitial?: string\n): CrdtText<string> => {\n\tconst doc = docFrom(replica, initial);\n\tconst text = doc.getText(TEXT_KEY);\n\t// State vector at the last sync point (a take or a merge). takeDelta encodes\n\t// every update since this vector and then advances it; merge advances it too,\n\t// so a remote op merged in isn't re-broadcast on the next take.\n\tlet lastVector = Y.encodeStateVector(doc);\n\n\treturn {\n\t\tanchorAt: (index) =>\n\t\t\ttoBase64(\n\t\t\t\tY.encodeRelativePosition(\n\t\t\t\t\tY.createRelativePositionFromTypeIndex(\n\t\t\t\t\t\ttext,\n\t\t\t\t\t\tMath.max(0, Math.min(index, text.length))\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t),\n\t\tindexOfAnchor: (anchor) => {\n\t\t\tif (anchor === null) return 0;\n\t\t\ttry {\n\t\t\t\tconst absolute = Y.createAbsolutePositionFromRelativePosition(\n\t\t\t\t\tY.decodeRelativePosition(fromBase64(anchor)),\n\t\t\t\t\tdoc\n\t\t\t\t);\n\n\t\t\t\treturn absolute?.type === text ? absolute.index : 0;\n\t\t\t} catch {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t},\n\t\tmerge: (state) => {\n\t\t\tapply(doc, state);\n\t\t\tlastVector = Y.encodeStateVector(doc);\n\t\t},\n\t\tsetText: (next) => doc.transact(() => reconcile(text, next)),\n\t\tstate: () => encode(doc),\n\t\ttakeDelta: () => {\n\t\t\tconst update = Y.encodeStateAsUpdate(doc, lastVector);\n\t\t\tlastVector = Y.encodeStateVector(doc);\n\n\t\t\treturn toBase64(update);\n\t\t},\n\t\ttext: () => text.toString()\n\t};\n};\n\n/**\n * The Yjs collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in for\n * the first-party `rgaText`: `create` mints a live doc, `merge` combines two\n * persisted states server-side, `empty`/`textOf` are conveniences.\n */\nexport const yjsText: TextCrdtAdapter<string> = {\n\tcreate: createYjsText,\n\tempty: () => encode(new Y.Doc()),\n\tmerge: (a, b) => {\n\t\tconst doc = new Y.Doc();\n\t\tapply(doc, a);\n\t\tapply(doc, b);\n\n\t\treturn encode(doc);\n\t},\n\ttextOf: (state) => {\n\t\tconst doc = new Y.Doc();\n\t\tapply(doc, state);\n\n\t\treturn doc.getText(TEXT_KEY).toString();\n\t}\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C99C343FC96084F064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|