@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 +10 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +85 -0
- package/package.json +54 -0
- package/src/index.ts +94 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -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 };
|