@syncular/crdt-yjs 0.2.0

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,10 @@
1
+ # @syncular/crdt-yjs
2
+
3
+ Yjs CRDT integration for Syncular columns.
4
+
5
+ Part of [Syncular](https://syncular.dev) — an offline-first sync framework.
6
+ See the [Syncular repository](https://github.com/syncular/syncular) for docs.
7
+
8
+ ## License
9
+
10
+ Apache-2.0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @syncular/crdt-yjs — the reference Yjs binding for CRDT columns
3
+ * (SPEC.md §5.10). Two faces of one Yjs integration:
4
+ *
5
+ * - the SERVER-side `yjsDocMerger` (§5.10.2): a `CrdtMerger` for `crdtType`
6
+ * `'yjs-doc'` — this is where Yjs is *required*, so it lives here, never in
7
+ * `@syncular/core` or `@syncular/server` (the blob-store rule,
8
+ * §5.9.2);
9
+ * - the CLIENT-side `YjsColumn` helper (§5.10.4): a thin `Y.Doc` wrapper an
10
+ * app uses to produce update bytes for a `crdt` column and to apply
11
+ * server-merged bytes back. Codegen has no Yjs dependency; this accessor
12
+ * does.
13
+ *
14
+ * Yjs enters the dependency tree exactly here.
15
+ */
16
+ import type { CrdtMerger, CrdtMergerRegistry } from '@syncular/server';
17
+ import * as Y from 'yjs';
18
+ /** The one built-in `crdtType` this rung defines (§5.10.1). */
19
+ export declare const YJS_DOC_CRDT_TYPE = "yjs-doc";
20
+ /**
21
+ * §5.10.2 merger for `crdtType` `'yjs-doc'`. `stored` and `incoming` are Yjs
22
+ * updates (or a full doc state — a state is a legal update). Merge = apply
23
+ * both into a fresh doc and re-encode the whole state as one update. This is
24
+ * commutative, associative, and idempotent (the Yjs CRDT contract), so
25
+ * concurrent pushes converge order-independently and a replayed update is a
26
+ * no-op (§5.10.3).
27
+ */
28
+ export declare const yjsDocMerger: CrdtMerger;
29
+ /** A registry pre-wired with the `yjs-doc` merger — pass to the server ctx
30
+ * `crdtMergers` (§5.10.2). Spread to add host mergers. */
31
+ export declare const yjsCrdtMergers: CrdtMergerRegistry;
32
+ /**
33
+ * §5.10.4 client helper: a `Y.Doc` bound to one `crdt` column value. The app
34
+ * mutates the doc (its shared types) and reads `columnBytes()` to store in a
35
+ * `mutate` (baseVersion-less, §5.10.3); on delivery of server-merged bytes it
36
+ * calls `applyServerBytes()`. The raw bytes are the transport; the doc is the
37
+ * app-visible collaborative value.
38
+ */
39
+ export declare class YjsColumn {
40
+ readonly doc: Y.Doc;
41
+ constructor(initial?: Uint8Array | null);
42
+ /** A shared text (the common collaborative-text case). */
43
+ text(name?: string): Y.Text;
44
+ /** A shared map. */
45
+ map(name?: string): Y.Map<unknown>;
46
+ /** The bytes to store in the `crdt` column (the full doc state as an
47
+ * update). Idempotent on the server merger, so pushing the whole state is
48
+ * safe even though it is larger than a per-edit delta — apps that want the
49
+ * minimal delta can diff with `Y.encodeStateAsUpdate(doc, stateVector)`. */
50
+ columnBytes(): Uint8Array;
51
+ /** Apply server-merged bytes (a pull COMMIT upsert, a segment row, or a
52
+ * conflict `serverRow`) into the local doc — idempotent (§5.10.4). */
53
+ applyServerBytes(bytes: Uint8Array): void;
54
+ destroy(): void;
55
+ }
56
+ export { Y };
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @syncular/crdt-yjs — the reference Yjs binding for CRDT columns
3
+ * (SPEC.md §5.10). Two faces of one Yjs integration:
4
+ *
5
+ * - the SERVER-side `yjsDocMerger` (§5.10.2): a `CrdtMerger` for `crdtType`
6
+ * `'yjs-doc'` — this is where Yjs is *required*, so it lives here, never in
7
+ * `@syncular/core` or `@syncular/server` (the blob-store rule,
8
+ * §5.9.2);
9
+ * - the CLIENT-side `YjsColumn` helper (§5.10.4): a thin `Y.Doc` wrapper an
10
+ * app uses to produce update bytes for a `crdt` column and to apply
11
+ * server-merged bytes back. Codegen has no Yjs dependency; this accessor
12
+ * does.
13
+ *
14
+ * Yjs enters the dependency tree exactly here.
15
+ */
16
+ import * as Y from 'yjs';
17
+ /** The one built-in `crdtType` this rung defines (§5.10.1). */
18
+ export const YJS_DOC_CRDT_TYPE = 'yjs-doc';
19
+ /**
20
+ * §5.10.2 merger for `crdtType` `'yjs-doc'`. `stored` and `incoming` are Yjs
21
+ * updates (or a full doc state — a state is a legal update). Merge = apply
22
+ * both into a fresh doc and re-encode the whole state as one update. This is
23
+ * commutative, associative, and idempotent (the Yjs CRDT contract), so
24
+ * concurrent pushes converge order-independently and a replayed update is a
25
+ * no-op (§5.10.3).
26
+ */
27
+ export const yjsDocMerger = (stored, incoming) => {
28
+ const doc = new Y.Doc();
29
+ try {
30
+ if (stored !== null && stored.length > 0)
31
+ Y.applyUpdate(doc, stored);
32
+ if (incoming.length > 0)
33
+ Y.applyUpdate(doc, incoming);
34
+ return Y.encodeStateAsUpdate(doc);
35
+ }
36
+ finally {
37
+ doc.destroy();
38
+ }
39
+ };
40
+ /** A registry pre-wired with the `yjs-doc` merger — pass to the server ctx
41
+ * `crdtMergers` (§5.10.2). Spread to add host mergers. */
42
+ export const yjsCrdtMergers = {
43
+ [YJS_DOC_CRDT_TYPE]: yjsDocMerger,
44
+ };
45
+ /**
46
+ * §5.10.4 client helper: a `Y.Doc` bound to one `crdt` column value. The app
47
+ * mutates the doc (its shared types) and reads `columnBytes()` to store in a
48
+ * `mutate` (baseVersion-less, §5.10.3); on delivery of server-merged bytes it
49
+ * calls `applyServerBytes()`. The raw bytes are the transport; the doc is the
50
+ * app-visible collaborative value.
51
+ */
52
+ export class YjsColumn {
53
+ doc;
54
+ constructor(initial) {
55
+ this.doc = new Y.Doc();
56
+ if (initial !== undefined && initial !== null && initial.length > 0) {
57
+ Y.applyUpdate(this.doc, initial);
58
+ }
59
+ }
60
+ /** A shared text (the common collaborative-text case). */
61
+ text(name = 'text') {
62
+ return this.doc.getText(name);
63
+ }
64
+ /** A shared map. */
65
+ map(name = 'map') {
66
+ return this.doc.getMap(name);
67
+ }
68
+ /** The bytes to store in the `crdt` column (the full doc state as an
69
+ * update). Idempotent on the server merger, so pushing the whole state is
70
+ * safe even though it is larger than a per-edit delta — apps that want the
71
+ * minimal delta can diff with `Y.encodeStateAsUpdate(doc, stateVector)`. */
72
+ columnBytes() {
73
+ return Y.encodeStateAsUpdate(this.doc);
74
+ }
75
+ /** Apply server-merged bytes (a pull COMMIT upsert, a segment row, or a
76
+ * conflict `serverRow`) into the local doc — idempotent (§5.10.4). */
77
+ applyServerBytes(bytes) {
78
+ if (bytes.length > 0)
79
+ Y.applyUpdate(this.doc, bytes);
80
+ }
81
+ destroy() {
82
+ this.doc.destroy();
83
+ }
84
+ }
85
+ export { Y };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@syncular/crdt-yjs",
3
+ "version": "0.2.0",
4
+ "description": "Yjs CRDT integration for Syncular columns",
5
+ "license": "Apache-2.0",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular.git",
11
+ "directory": "packages/crdt-yjs"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular/issues"
15
+ },
16
+ "keywords": [
17
+ "sync",
18
+ "offline-first",
19
+ "realtime",
20
+ "database",
21
+ "typescript"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "browser": "./src/index.ts",
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "README.md",
42
+ "!src/**/*.test.ts",
43
+ "!src/**/*.test.tsx",
44
+ "!dist/**/*.test.js",
45
+ "!dist/**/*.test.d.ts"
46
+ ],
47
+ "dependencies": {
48
+ "@syncular/core": "0.2.0",
49
+ "yjs": "^13.6.0"
50
+ },
51
+ "devDependencies": {
52
+ "@syncular/server": "0.2.0"
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @syncular/crdt-yjs — the reference Yjs binding for CRDT columns
3
+ * (SPEC.md §5.10). Two faces of one Yjs integration:
4
+ *
5
+ * - the SERVER-side `yjsDocMerger` (§5.10.2): a `CrdtMerger` for `crdtType`
6
+ * `'yjs-doc'` — this is where Yjs is *required*, so it lives here, never in
7
+ * `@syncular/core` or `@syncular/server` (the blob-store rule,
8
+ * §5.9.2);
9
+ * - the CLIENT-side `YjsColumn` helper (§5.10.4): a thin `Y.Doc` wrapper an
10
+ * app uses to produce update bytes for a `crdt` column and to apply
11
+ * server-merged bytes back. Codegen has no Yjs dependency; this accessor
12
+ * does.
13
+ *
14
+ * Yjs enters the dependency tree exactly here.
15
+ */
16
+
17
+ import type { CrdtMerger, CrdtMergerRegistry } from '@syncular/server';
18
+ import * as Y from 'yjs';
19
+
20
+ /** The one built-in `crdtType` this rung defines (§5.10.1). */
21
+ export const YJS_DOC_CRDT_TYPE = 'yjs-doc';
22
+
23
+ /**
24
+ * §5.10.2 merger for `crdtType` `'yjs-doc'`. `stored` and `incoming` are Yjs
25
+ * updates (or a full doc state — a state is a legal update). Merge = apply
26
+ * both into a fresh doc and re-encode the whole state as one update. This is
27
+ * commutative, associative, and idempotent (the Yjs CRDT contract), so
28
+ * concurrent pushes converge order-independently and a replayed update is a
29
+ * no-op (§5.10.3).
30
+ */
31
+ export const yjsDocMerger: CrdtMerger = (stored, incoming) => {
32
+ const doc = new Y.Doc();
33
+ try {
34
+ if (stored !== null && stored.length > 0) Y.applyUpdate(doc, stored);
35
+ if (incoming.length > 0) Y.applyUpdate(doc, incoming);
36
+ return Y.encodeStateAsUpdate(doc);
37
+ } finally {
38
+ doc.destroy();
39
+ }
40
+ };
41
+
42
+ /** A registry pre-wired with the `yjs-doc` merger — pass to the server ctx
43
+ * `crdtMergers` (§5.10.2). Spread to add host mergers. */
44
+ export const yjsCrdtMergers: CrdtMergerRegistry = {
45
+ [YJS_DOC_CRDT_TYPE]: yjsDocMerger,
46
+ };
47
+
48
+ /**
49
+ * §5.10.4 client helper: a `Y.Doc` bound to one `crdt` column value. The app
50
+ * mutates the doc (its shared types) and reads `columnBytes()` to store in a
51
+ * `mutate` (baseVersion-less, §5.10.3); on delivery of server-merged bytes it
52
+ * calls `applyServerBytes()`. The raw bytes are the transport; the doc is the
53
+ * app-visible collaborative value.
54
+ */
55
+ export class YjsColumn {
56
+ readonly doc: Y.Doc;
57
+
58
+ constructor(initial?: Uint8Array | null) {
59
+ this.doc = new Y.Doc();
60
+ if (initial !== undefined && initial !== null && initial.length > 0) {
61
+ Y.applyUpdate(this.doc, initial);
62
+ }
63
+ }
64
+
65
+ /** A shared text (the common collaborative-text case). */
66
+ text(name = 'text'): Y.Text {
67
+ return this.doc.getText(name);
68
+ }
69
+
70
+ /** A shared map. */
71
+ map(name = 'map'): Y.Map<unknown> {
72
+ return this.doc.getMap(name);
73
+ }
74
+
75
+ /** The bytes to store in the `crdt` column (the full doc state as an
76
+ * update). Idempotent on the server merger, so pushing the whole state is
77
+ * safe even though it is larger than a per-edit delta — apps that want the
78
+ * minimal delta can diff with `Y.encodeStateAsUpdate(doc, stateVector)`. */
79
+ columnBytes(): Uint8Array {
80
+ return Y.encodeStateAsUpdate(this.doc);
81
+ }
82
+
83
+ /** Apply server-merged bytes (a pull COMMIT upsert, a segment row, or a
84
+ * conflict `serverRow`) into the local doc — idempotent (§5.10.4). */
85
+ applyServerBytes(bytes: Uint8Array): void {
86
+ if (bytes.length > 0) Y.applyUpdate(this.doc, bytes);
87
+ }
88
+
89
+ destroy(): void {
90
+ this.doc.destroy();
91
+ }
92
+ }
93
+
94
+ export { Y };