@absolutejs/sync-yjs 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,57 @@
1
+ # @absolutejs/sync-yjs
2
+
3
+ A [Yjs](https://github.com/yjs/yjs)-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
+
7
+ `@absolutejs/sync/crdt` ships a first-party RGA text CRDT that's great for
8
+ offline-merge and moderate collaboration. When you need production-scale
9
+ collaborative text — efficient deltas, no unbounded tombstone growth, the
10
+ interleaving guarantees the community relies on — Yjs is the staple. This adapter
11
+ lets you swap it in without touching your call sites.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bun add @absolutejs/sync-yjs yjs
17
+ ```
18
+
19
+ `@absolutejs/sync` is a peer dependency (you already have it). `yjs` is a runtime
20
+ dependency you install alongside.
21
+
22
+ ## Use
23
+
24
+ ```ts
25
+ import { yjsText } from '@absolutejs/sync-yjs';
26
+ // ...instead of: import { rgaText } from '@absolutejs/sync/crdt';
27
+
28
+ // Client — a live doc per replica (e.g. per tab/device)
29
+ const doc = yjsText.create('tab-a', initialStateFromServer);
30
+ doc.setText(textarea.value); // reconciles via a minimal diff
31
+ socket.send(doc.state()); // broadcast the serialized (base64) state
32
+ doc.merge(incomingState); // fold a peer's state into local edits
33
+ doc.text(); // the merged visible text
34
+
35
+ // Server — merge-on-write so concurrent writes combine instead of clobbering
36
+ const next = yjsText.merge(storedState, incomingState);
37
+ // store `next`; yjsText.textOf(next) is the plain text
38
+ ```
39
+
40
+ The serialized state is a base64 string of a Yjs update — JSON-safe for the sync
41
+ engine's change feed and for storing as a row field. `merge` is
42
+ commutative/associative/idempotent, so the order writes arrive in doesn't matter.
43
+
44
+ ## The contract
45
+
46
+ This package implements `TextCrdtAdapter<string>` from `@absolutejs/sync/crdt`:
47
+
48
+ | Member | What it does |
49
+ | --------------------------- | ------------------------------------------------------------- |
50
+ | `create(replica, initial?)` | A live `CrdtText` doc: `text/setText/merge/state`. |
51
+ | `merge(a, b)` | Combine two persisted states (server-side, no live instance). |
52
+ | `empty()` | The serialized state of an empty document. |
53
+ | `textOf(state)` | The plain text of a persisted state. |
54
+
55
+ ## License
56
+
57
+ CC BY-NC 4.0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A Yjs-backed collaborative-text CRDT for `@absolutejs/sync`.
3
+ *
4
+ * `@absolutejs/sync` ships a zero-dependency RGA text CRDT (`rgaText`) that is
5
+ * fine for offline-merge and moderate collaboration. Yjs is the community staple
6
+ * for production-scale collaborative text — it solves the real footguns (tombstone
7
+ * growth, efficient deltas, interleaving). This adapter wraps Yjs behind the exact
8
+ * same `CrdtText` / `TextCrdtAdapter` contract, so swapping `rgaText` for `yjsText`
9
+ * needs no other change at the call site.
10
+ *
11
+ * The serialized state is a **base64 string** of a Yjs update, so it stays
12
+ * JSON-safe for transport over the sync engine's change feed and storage as a row
13
+ * field. Merges are commutative/associative/idempotent (Yjs guarantees it), so a
14
+ * merge-on-write mutation combines concurrent edits without clobbering.
15
+ */
16
+ import type { CrdtText, TextCrdtAdapter } from '@absolutejs/sync/crdt';
17
+ /** Create a live Yjs-backed collaborative-text doc for `replica`. */
18
+ export declare const createYjsText: (replica: string, initial?: string) => CrdtText<string>;
19
+ /**
20
+ * The Yjs collaborative-text backend as a {@link TextCrdtAdapter}. Drop-in for
21
+ * the first-party `rgaText`: `create` mints a live doc, `merge` combines two
22
+ * persisted states server-side, `empty`/`textOf` are conveniences.
23
+ */
24
+ export declare const yjsText: TextCrdtAdapter<string>;
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import * as Y from "yjs";
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 encode = (doc) => toBase64(Y.encodeStateAsUpdate(doc));
21
+ var apply = (doc, state) => {
22
+ if (state.length > 0) {
23
+ Y.applyUpdate(doc, fromBase64(state));
24
+ }
25
+ };
26
+ var clientIdFor = (replica) => {
27
+ let hash = 0;
28
+ for (let index = 0;index < replica.length; index += 1) {
29
+ hash = Math.imul(hash, 31) + replica.charCodeAt(index) | 0;
30
+ }
31
+ return hash >>> 0;
32
+ };
33
+ var docFrom = (replica, initial) => {
34
+ const doc = new Y.Doc;
35
+ doc.clientID = clientIdFor(replica);
36
+ if (initial !== undefined) {
37
+ apply(doc, initial);
38
+ }
39
+ return doc;
40
+ };
41
+ var reconcile = (text, next) => {
42
+ const current = text.toString();
43
+ if (current === next) {
44
+ return;
45
+ }
46
+ let prefix = 0;
47
+ const maxPrefix = Math.min(current.length, next.length);
48
+ while (prefix < maxPrefix && current[prefix] === next[prefix]) {
49
+ prefix += 1;
50
+ }
51
+ let suffix = 0;
52
+ while (suffix < maxPrefix - prefix && current[current.length - 1 - suffix] === next[next.length - 1 - suffix]) {
53
+ suffix += 1;
54
+ }
55
+ const removed = current.length - prefix - suffix;
56
+ const inserted = next.slice(prefix, next.length - suffix);
57
+ if (removed > 0) {
58
+ text.delete(prefix, removed);
59
+ }
60
+ if (inserted.length > 0) {
61
+ text.insert(prefix, inserted);
62
+ }
63
+ };
64
+ var createYjsText = (replica, initial) => {
65
+ const doc = docFrom(replica, initial);
66
+ const text = doc.getText(TEXT_KEY);
67
+ return {
68
+ merge: (state) => apply(doc, state),
69
+ setText: (next) => doc.transact(() => reconcile(text, next)),
70
+ state: () => encode(doc),
71
+ text: () => text.toString()
72
+ };
73
+ };
74
+ var yjsText = {
75
+ create: createYjsText,
76
+ empty: () => encode(new Y.Doc),
77
+ merge: (a, b) => {
78
+ const doc = new Y.Doc;
79
+ apply(doc, a);
80
+ apply(doc, b);
81
+ return encode(doc);
82
+ },
83
+ textOf: (state) => {
84
+ const doc = new Y.Doc;
85
+ apply(doc, state);
86
+ return doc.getText(TEXT_KEY).toString();
87
+ }
88
+ };
89
+ export {
90
+ yjsText,
91
+ createYjsText
92
+ };
93
+
94
+ //# debugId=B68ED6342B46BD1064756E2164756E21
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
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\n\treturn {\n\t\tmerge: (state) => apply(doc, state),\n\t\tsetText: (next) => doc.transact(() => reconcile(text, next)),\n\t\tstate: () => encode(doc),\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
+ ],
7
+ "mappings": "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",
8
+ "debugId": "B68ED6342B46BD1064756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@absolutejs/sync-yjs",
3
+ "version": "0.0.1",
4
+ "description": "Yjs-backed collaborative-text CRDT adapter for @absolutejs/sync",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/sync-adapters.git",
8
+ "directory": "yjs"
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
+ "yjs",
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 yjs && 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
+ "yjs": "^13.6.30"
36
+ },
37
+ "peerDependencies": {
38
+ "@absolutejs/sync": ">= 0.10.0"
39
+ },
40
+ "devDependencies": {
41
+ "@absolutejs/sync": "^0.10.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
+ }