@kyneta/yjs-schema 1.3.0 → 1.4.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 +28 -25
- package/dist/index.d.ts +185 -86
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1159 -860
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/bind-constraints.test.ts +39 -30
- package/src/__tests__/bind-yjs.test.ts +53 -16
- package/src/__tests__/position.test.ts +376 -0
- package/src/__tests__/structural-merge.test.ts +111 -54
- package/src/__tests__/substrate.test.ts +18 -0
- package/src/__tests__/version.test.ts +87 -0
- package/src/bind-yjs.ts +44 -37
- package/src/change-mapping.ts +219 -25
- package/src/index.ts +3 -1
- package/src/populate.ts +59 -12
- package/src/position.ts +45 -0
- package/src/reader.ts +62 -6
- package/src/substrate.ts +99 -11
- package/src/version.ts +135 -33
- package/src/yjs-resolve.ts +59 -12
package/dist/index.js
CHANGED
|
@@ -1,892 +1,1191 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
applyChanges,
|
|
4
|
-
change,
|
|
5
|
-
createDoc,
|
|
6
|
-
createRef,
|
|
7
|
-
exportEntirety,
|
|
8
|
-
exportSince,
|
|
9
|
-
merge,
|
|
10
|
-
NATIVE,
|
|
11
|
-
Schema,
|
|
12
|
-
subscribe,
|
|
13
|
-
subscribeNode,
|
|
14
|
-
unwrap,
|
|
15
|
-
version
|
|
16
|
-
} from "@kyneta/schema";
|
|
17
|
-
|
|
18
|
-
// src/bind-yjs.ts
|
|
19
|
-
import {
|
|
20
|
-
BACKING_DOC as BACKING_DOC2,
|
|
21
|
-
createSubstrateNamespace,
|
|
22
|
-
STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2
|
|
23
|
-
} from "@kyneta/schema";
|
|
24
|
-
import * as Y6 from "yjs";
|
|
25
|
-
|
|
26
|
-
// src/populate.ts
|
|
27
|
-
import { KIND, STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
|
|
1
|
+
import { BACKING_DOC, KIND, NATIVE, RawPath, STRUCTURAL_YJS_CLIENT_ID, SYNC_COLLABORATIVE, Schema, Zero, advanceSchema, applyChanges, base64ToUint8Array, buildWritableContext, change, createBindingTarget, createDoc, createRef, deriveSchemaBinding, executeBatch, expandMapOpsToLeaves, exportEntirety, exportSince, merge, richTextChange, subscribe, subscribeNode, uint8ArrayToBase64, unwrap, version, versionVectorCompare, versionVectorMeet } from "@kyneta/schema";
|
|
28
2
|
import * as Y from "yjs";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
function ensureMapContainers(schema) {
|
|
81
|
-
const map = new Y.Map();
|
|
82
|
-
if (schema[KIND] !== "product") return map;
|
|
83
|
-
for (const [key, fieldSchema] of Object.entries(
|
|
84
|
-
schema.fields
|
|
85
|
-
).sort(([a], [b]) => a.localeCompare(b))) {
|
|
86
|
-
switch (fieldSchema[KIND]) {
|
|
87
|
-
case "text":
|
|
88
|
-
map.set(key, new Y.Text());
|
|
89
|
-
break;
|
|
90
|
-
case "product":
|
|
91
|
-
map.set(key, ensureMapContainers(fieldSchema));
|
|
92
|
-
break;
|
|
93
|
-
case "sequence":
|
|
94
|
-
map.set(key, new Y.Array());
|
|
95
|
-
break;
|
|
96
|
-
case "map":
|
|
97
|
-
map.set(key, new Y.Map());
|
|
98
|
-
break;
|
|
99
|
-
case "scalar":
|
|
100
|
-
case "sum": {
|
|
101
|
-
const zero = Zero.structural(fieldSchema);
|
|
102
|
-
if (zero !== void 0) {
|
|
103
|
-
map.set(key, zero);
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case "counter":
|
|
108
|
-
case "set":
|
|
109
|
-
case "tree":
|
|
110
|
-
case "movable":
|
|
111
|
-
throw new Error(
|
|
112
|
-
`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". Supported kinds: text, product, sequence, map, scalar, sum. Encountered unsupported kind at nested field "${key}".`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return map;
|
|
3
|
+
import { createSnapshot, decodeStateVector, encodeSnapshot, encodeStateVector, snapshot } from "yjs";
|
|
4
|
+
//#region src/populate.ts
|
|
5
|
+
/**
|
|
6
|
+
* Ensure that a Y.Doc's root map contains the correct Yjs shared types
|
|
7
|
+
* matching the schema structure.
|
|
8
|
+
*
|
|
9
|
+
* Obtains the root map via `doc.getMap("root")`, reads the root product
|
|
10
|
+
* schema's fields, and creates empty containers for each field within a
|
|
11
|
+
* single `doc.transact()` call for atomicity.
|
|
12
|
+
*
|
|
13
|
+
* When `conditional` is true, fields that already exist in the root map
|
|
14
|
+
* are skipped. This is the correct mode after hydration — containers
|
|
15
|
+
* present from stored state must not be overwritten (each `rootMap.set()`
|
|
16
|
+
* is a CRDT write that advances the version vector and may conflict
|
|
17
|
+
* with stored operations).
|
|
18
|
+
*
|
|
19
|
+
* When `conditional` is false (default), all fields are created
|
|
20
|
+
* unconditionally. This is the correct mode for fresh documents.
|
|
21
|
+
*
|
|
22
|
+
* **Structural identity:** This function temporarily sets `doc.clientID`
|
|
23
|
+
* to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
|
|
24
|
+
* then restores the caller's clientID. This produces byte-identical
|
|
25
|
+
* structural ops across all peers, enabling Yjs deduplication on merge.
|
|
26
|
+
*
|
|
27
|
+
* **Identity-keying:** When a `binding` is provided, each root field's
|
|
28
|
+
* key in the root Y.Map is the identity hash from `binding.forward`
|
|
29
|
+
* instead of the field name. Nested product fields are similarly keyed
|
|
30
|
+
* via `ensureMapContainers`.
|
|
31
|
+
*
|
|
32
|
+
* @param doc - The Y.Doc to prepare
|
|
33
|
+
* @param schema - The root document schema (a ProductSchema)
|
|
34
|
+
* @param conditional - If true, skip fields that already exist in the root map.
|
|
35
|
+
* Context: jj:smmulzkm (two-phase substrate construction)
|
|
36
|
+
* @param binding - Optional SchemaBinding for identity-keyed containers.
|
|
37
|
+
*/
|
|
38
|
+
function ensureContainers(doc, schema, conditional = false, binding) {
|
|
39
|
+
const rootMap = doc.getMap("root");
|
|
40
|
+
if (schema[KIND] !== "product") return;
|
|
41
|
+
const savedClientID = doc.clientID;
|
|
42
|
+
doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
|
|
43
|
+
try {
|
|
44
|
+
doc.transact(() => {
|
|
45
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b))) {
|
|
46
|
+
const mapKey = binding?.forward.get(key) ?? key;
|
|
47
|
+
if (conditional && rootMap.has(mapKey)) continue;
|
|
48
|
+
ensureRootField(rootMap, mapKey, fieldSchema, binding, key);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
doc.clientID = savedClientID;
|
|
53
|
+
}
|
|
117
54
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
return current;
|
|
55
|
+
/**
|
|
56
|
+
* Ensure a root-level Yjs shared type exists for a schema field.
|
|
57
|
+
*
|
|
58
|
+
* Dispatches on `[KIND]`:
|
|
59
|
+
* - `"text"` → empty Y.Text
|
|
60
|
+
* - `"product"` → empty Y.Map (recursive for nested products)
|
|
61
|
+
* - `"sequence"` → empty Y.Array
|
|
62
|
+
* - `"map"` → empty Y.Map
|
|
63
|
+
* - `"scalar"` / `"sum"` → Zero.structural default
|
|
64
|
+
* - `"counter"` / `"set"` / `"tree"` / `"movable"` → throw (not supported by Yjs)
|
|
65
|
+
*
|
|
66
|
+
* @param rootMap - The root Y.Map to set the field on.
|
|
67
|
+
* @param key - The key to use in the root map (identity hash or field name).
|
|
68
|
+
* @param fieldSchema - The schema for this field.
|
|
69
|
+
* @param binding - Optional SchemaBinding for nested identity-keying.
|
|
70
|
+
* @param prefix - The absolute schema path prefix for this field (used for nested lookups).
|
|
71
|
+
*/
|
|
72
|
+
function ensureRootField(rootMap, key, fieldSchema, binding, prefix) {
|
|
73
|
+
switch (fieldSchema[KIND]) {
|
|
74
|
+
case "text":
|
|
75
|
+
case "richtext":
|
|
76
|
+
rootMap.set(key, new Y.Text());
|
|
77
|
+
return;
|
|
78
|
+
case "product":
|
|
79
|
+
rootMap.set(key, ensureMapContainers(fieldSchema, binding, prefix));
|
|
80
|
+
return;
|
|
81
|
+
case "sequence":
|
|
82
|
+
rootMap.set(key, new Y.Array());
|
|
83
|
+
return;
|
|
84
|
+
case "map":
|
|
85
|
+
rootMap.set(key, new Y.Map());
|
|
86
|
+
return;
|
|
87
|
+
case "scalar":
|
|
88
|
+
case "sum": {
|
|
89
|
+
const zero = Zero.structural(fieldSchema);
|
|
90
|
+
if (zero !== void 0) rootMap.set(key, zero);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "counter":
|
|
94
|
+
case "set":
|
|
95
|
+
case "tree":
|
|
96
|
+
case "movable": throw new Error(`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". Supported kinds: text, richtext, product, sequence, map, scalar, sum. Encountered unsupported kind at root field "${key}".`);
|
|
97
|
+
}
|
|
163
98
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Create an empty Y.Map with nested shared type children matching
|
|
101
|
+
* the product schema's field structure.
|
|
102
|
+
*
|
|
103
|
+
* Only creates containers for fields that require Yjs shared types
|
|
104
|
+
* (text → Y.Text, product → Y.Map, sequence → Y.Array, map → Y.Map).
|
|
105
|
+
* Scalar and sum fields are set to their structural zero defaults.
|
|
106
|
+
*
|
|
107
|
+
* **Identity-keying:** When a `binding` is provided, computes the
|
|
108
|
+
* absolute schema path for each nested field (`prefix.fieldName`) and
|
|
109
|
+
* looks up the identity hash from `binding.forward`. The identity hash
|
|
110
|
+
* is used as the Y.Map entry key instead of the field name.
|
|
111
|
+
*
|
|
112
|
+
* @param schema - The product schema for this nested map.
|
|
113
|
+
* @param binding - Optional SchemaBinding for identity-keyed containers.
|
|
114
|
+
* @param prefix - The absolute schema path prefix (e.g. "meta" for fields under meta).
|
|
115
|
+
*/
|
|
116
|
+
function ensureMapContainers(schema, binding, prefix) {
|
|
117
|
+
const map = new Y.Map();
|
|
118
|
+
if (schema[KIND] !== "product") return map;
|
|
119
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b))) {
|
|
120
|
+
const absPath = prefix ? `${prefix}.${key}` : key;
|
|
121
|
+
const mapKey = binding?.forward.get(absPath) ?? key;
|
|
122
|
+
switch (fieldSchema[KIND]) {
|
|
123
|
+
case "text":
|
|
124
|
+
case "richtext":
|
|
125
|
+
map.set(mapKey, new Y.Text());
|
|
126
|
+
break;
|
|
127
|
+
case "product":
|
|
128
|
+
map.set(mapKey, ensureMapContainers(fieldSchema, binding, absPath));
|
|
129
|
+
break;
|
|
130
|
+
case "sequence":
|
|
131
|
+
map.set(mapKey, new Y.Array());
|
|
132
|
+
break;
|
|
133
|
+
case "map":
|
|
134
|
+
map.set(mapKey, new Y.Map());
|
|
135
|
+
break;
|
|
136
|
+
case "scalar":
|
|
137
|
+
case "sum": {
|
|
138
|
+
const zero = Zero.structural(fieldSchema);
|
|
139
|
+
if (zero !== void 0) map.set(mapKey, zero);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case "counter":
|
|
143
|
+
case "set":
|
|
144
|
+
case "tree":
|
|
145
|
+
case "movable": throw new Error(`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". Supported kinds: text, richtext, product, sequence, map, scalar, sum. Encountered unsupported kind at nested field "${key}".`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return map;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/yjs-resolve.ts
|
|
152
|
+
/**
|
|
153
|
+
* Navigate one step deeper into the Yjs shared type tree.
|
|
154
|
+
*
|
|
155
|
+
* Uses `instanceof` for runtime type discrimination:
|
|
156
|
+
* - `Y.Map` → `.get(key)` — uses the identity hash when provided
|
|
157
|
+
* - `Y.Array` → `.get(index)`
|
|
158
|
+
* - `Y.Text` → terminal (cannot step further)
|
|
159
|
+
* - Plain value → terminal (return `undefined`)
|
|
160
|
+
*
|
|
161
|
+
* @param current - The current position (a Yjs shared type or plain value)
|
|
162
|
+
* @param segment - The path segment to follow
|
|
163
|
+
* @param identity - Optional identity hash to use instead of the segment's resolved value
|
|
164
|
+
*/
|
|
165
|
+
function stepIntoYjs(current, segment, identity) {
|
|
166
|
+
const resolved = segment.resolve();
|
|
167
|
+
if (current instanceof Y.Map) return current.get(identity ?? resolved);
|
|
168
|
+
if (current instanceof Y.Array) return current.get(resolved);
|
|
169
|
+
if (current instanceof Y.Text) throw new Error(`yjs-resolve: cannot step into Y.Text`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolve a Yjs shared type (or plain value) at the given path.
|
|
173
|
+
*
|
|
174
|
+
* Left-folds over path segments using `advanceSchema` for pure schema
|
|
175
|
+
* descent and `stepIntoYjs` for Yjs-specific navigation.
|
|
176
|
+
*
|
|
177
|
+
* When a `binding` is provided, each step computes the absolute schema
|
|
178
|
+
* path and looks up the identity hash from `binding.forward`. This
|
|
179
|
+
* identity hash is used instead of the field name at every product-field
|
|
180
|
+
* boundary (root and nested).
|
|
181
|
+
*
|
|
182
|
+
* Returns both the Yjs shared type (or plain value) and the schema at
|
|
183
|
+
* the terminal position. For an empty path, returns the root map and
|
|
184
|
+
* root schema.
|
|
185
|
+
*
|
|
186
|
+
* @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
|
|
187
|
+
* @param rootSchema - The root document schema
|
|
188
|
+
* @param path - The path to resolve
|
|
189
|
+
* @param binding - Optional SchemaBinding for identity-keyed navigation.
|
|
190
|
+
*/
|
|
191
|
+
function resolveYjsType(rootMap, rootSchema, path, binding) {
|
|
192
|
+
let current = rootMap;
|
|
193
|
+
let schema = rootSchema;
|
|
194
|
+
let absPath = "";
|
|
195
|
+
for (let i = 0; i < path.length; i++) {
|
|
196
|
+
const seg = path.segments[i];
|
|
197
|
+
if (!seg) throw new Error(`Missing segment at index ${i}`);
|
|
198
|
+
const nextSchema = advanceSchema(schema, seg);
|
|
199
|
+
let identity;
|
|
200
|
+
if (binding && seg.role === "key") {
|
|
201
|
+
const segStr = seg.resolve();
|
|
202
|
+
absPath = absPath ? `${absPath}.${segStr}` : segStr;
|
|
203
|
+
identity = binding.forward.get(absPath);
|
|
204
|
+
}
|
|
205
|
+
current = stepIntoYjs(current, seg, identity);
|
|
206
|
+
schema = nextSchema;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
resolved: current,
|
|
210
|
+
schema
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/change-mapping.ts
|
|
215
|
+
/**
|
|
216
|
+
* Apply a kyneta Change to the Yjs shared type tree imperatively.
|
|
217
|
+
*
|
|
218
|
+
* Resolves the target shared type at `path`, then applies the change
|
|
219
|
+
* via the appropriate Yjs API. Must be called within a `doc.transact()`
|
|
220
|
+
* for atomicity and correct event batching.
|
|
221
|
+
*
|
|
222
|
+
* @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
|
|
223
|
+
* @param rootSchema - The root document schema
|
|
224
|
+
* @param path - The path to the target
|
|
225
|
+
* @param change - The kyneta Change to apply
|
|
226
|
+
*/
|
|
227
|
+
function applyChangeToYjs(rootMap, rootSchema, path, change, binding) {
|
|
228
|
+
switch (change.type) {
|
|
229
|
+
case "text":
|
|
230
|
+
applyTextChange(rootMap, rootSchema, path, change, binding);
|
|
231
|
+
return;
|
|
232
|
+
case "richtext":
|
|
233
|
+
applyRichTextChange(rootMap, rootSchema, path, change, binding);
|
|
234
|
+
return;
|
|
235
|
+
case "sequence":
|
|
236
|
+
applySequenceChange(rootMap, rootSchema, path, change, binding);
|
|
237
|
+
return;
|
|
238
|
+
case "map":
|
|
239
|
+
applyMapChange(rootMap, rootSchema, path, change, binding);
|
|
240
|
+
return;
|
|
241
|
+
case "replace":
|
|
242
|
+
applyReplaceChange(rootMap, rootSchema, path, change, binding);
|
|
243
|
+
return;
|
|
244
|
+
case "increment": throw new Error(`Yjs substrate does not support "${change.type}" changes. Counter requires a CRDT backend that supports counters (e.g. Loro). Attempted IncrementChange with amount=${change.amount} at path [${pathToString(path)}].`);
|
|
245
|
+
case "tree": throw new Error(`Yjs substrate does not support "${change.type}" changes. Tree requires a CRDT backend that supports trees (e.g. Loro). Attempted TreeChange at path [${pathToString(path)}].`);
|
|
246
|
+
default: throw new Error(`applyChangeToYjs: unsupported change type "${change.type}"`);
|
|
247
|
+
}
|
|
273
248
|
}
|
|
249
|
+
function applyTextChange(rootMap, rootSchema, path, change, binding) {
|
|
250
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
251
|
+
if (!(resolved instanceof Y.Text)) throw new Error(`applyChangeToYjs: TextChange target at path [${pathToString(path)}] is not a Y.Text`);
|
|
252
|
+
resolved.applyDelta(change.instructions);
|
|
253
|
+
}
|
|
254
|
+
function applyRichTextChange(rootMap, rootSchema, path, change, binding) {
|
|
255
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
256
|
+
if (!(resolved instanceof Y.Text)) throw new Error(`applyChangeToYjs: RichTextChange target at path [${pathToString(path)}] is not a Y.Text`);
|
|
257
|
+
const delta = change.instructions.map((inst) => {
|
|
258
|
+
if ("retain" in inst) return { retain: inst.retain };
|
|
259
|
+
if ("format" in inst) return {
|
|
260
|
+
retain: inst.format,
|
|
261
|
+
attributes: inst.marks
|
|
262
|
+
};
|
|
263
|
+
if ("insert" in inst) {
|
|
264
|
+
const d = { insert: inst.insert };
|
|
265
|
+
if (inst.marks && Object.keys(inst.marks).length > 0) d.attributes = inst.marks;
|
|
266
|
+
return d;
|
|
267
|
+
}
|
|
268
|
+
if ("delete" in inst) return { delete: inst.delete };
|
|
269
|
+
throw new Error("applyRichTextChange: unknown instruction type");
|
|
270
|
+
});
|
|
271
|
+
resolved.applyDelta(delta);
|
|
272
|
+
}
|
|
273
|
+
function applySequenceChange(rootMap, rootSchema, path, change, binding) {
|
|
274
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
275
|
+
if (!(resolved instanceof Y.Array)) throw new Error(`applyChangeToYjs: SequenceChange target at path [${pathToString(path)}] is not a Y.Array`);
|
|
276
|
+
const itemSchema = getItemSchema(resolveSchemaAtPath(rootSchema, path));
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
for (const instruction of change.instructions) if ("retain" in instruction) cursor += instruction.retain;
|
|
279
|
+
else if ("delete" in instruction) resolved.delete(cursor, instruction.delete);
|
|
280
|
+
else if ("insert" in instruction) {
|
|
281
|
+
const items = instruction.insert;
|
|
282
|
+
const yjsItems = items.map((item) => maybeCreateSharedType(item, itemSchema));
|
|
283
|
+
resolved.insert(cursor, yjsItems);
|
|
284
|
+
cursor += items.length;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function applyMapChange(rootMap, rootSchema, path, change, binding) {
|
|
288
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
289
|
+
if (!(resolved instanceof Y.Map)) throw new Error(`applyChangeToYjs: MapChange target at path [${pathToString(path)}] is not a Y.Map`);
|
|
290
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
291
|
+
if (change.delete) for (const key of change.delete) resolved.delete(key);
|
|
292
|
+
if (change.set) for (const [key, value] of Object.entries(change.set)) {
|
|
293
|
+
const yjsValue = maybeCreateSharedType(value, getFieldSchema(targetSchema, key));
|
|
294
|
+
let mapKey = key;
|
|
295
|
+
if (binding && targetSchema[KIND] === "product") {
|
|
296
|
+
const parentAbsPath = path.segments.filter((s) => s.role === "key").map((s) => s.resolve()).join(".");
|
|
297
|
+
const absPath = parentAbsPath ? `${parentAbsPath}.${key}` : key;
|
|
298
|
+
const identity = binding.forward.get(absPath);
|
|
299
|
+
if (identity) mapKey = identity;
|
|
300
|
+
}
|
|
301
|
+
resolved.set(mapKey, yjsValue);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function applyReplaceChange(rootMap, rootSchema, path, change, binding) {
|
|
305
|
+
if (path.length === 0) throw new Error("applyChangeToYjs: ReplaceChange at root path is not supported");
|
|
306
|
+
const lastSeg = path.segments.at(-1);
|
|
307
|
+
if (!lastSeg) throw new Error("replaceChangeToDiff: empty path");
|
|
308
|
+
const parentPath = path.slice(0, -1);
|
|
309
|
+
const { resolved: parent } = resolveYjsType(rootMap, rootSchema, parentPath, binding);
|
|
310
|
+
const resolved = lastSeg.resolve();
|
|
311
|
+
if (parent instanceof Y.Map && lastSeg.role === "key") {
|
|
312
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
313
|
+
const yjsValue = maybeCreateSharedType(change.value, targetSchema);
|
|
314
|
+
let mapKey = resolved;
|
|
315
|
+
if (binding) {
|
|
316
|
+
const absPath = path.segments.filter((s) => s.role === "key").map((s) => s.resolve()).join(".");
|
|
317
|
+
const identity = binding.forward.get(absPath);
|
|
318
|
+
if (identity) mapKey = identity;
|
|
319
|
+
}
|
|
320
|
+
parent.set(mapKey, yjsValue);
|
|
321
|
+
} else if (parent instanceof Y.Array && lastSeg.role === "index") {
|
|
322
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
323
|
+
const yjsValue = maybeCreateSharedType(change.value, targetSchema);
|
|
324
|
+
parent.delete(resolved, 1);
|
|
325
|
+
parent.insert(resolved, [yjsValue]);
|
|
326
|
+
} else throw new Error(`applyChangeToYjs: ReplaceChange parent at path [${pathToString(parentPath)}] is not a Y.Map or Y.Array (got ${typeof parent})`);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* If the schema says the value should be a shared type (product → Y.Map,
|
|
330
|
+
* sequence → Y.Array, text → Y.Text, richtext → Y.Text), create and
|
|
331
|
+
* populate it. Otherwise return the plain value as-is.
|
|
332
|
+
*
|
|
333
|
+
* Uses populate-then-attach: the new shared type is fully populated
|
|
334
|
+
* before being returned for insertion into its parent.
|
|
335
|
+
*/
|
|
274
336
|
function maybeCreateSharedType(value, schema) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`Yjs substrate does not support [KIND]="${schema[KIND2]}". This should have been caught at bind() time.`
|
|
320
|
-
);
|
|
321
|
-
default:
|
|
322
|
-
return value;
|
|
323
|
-
}
|
|
337
|
+
if (schema === void 0) return value;
|
|
338
|
+
switch (schema[KIND]) {
|
|
339
|
+
case "text": {
|
|
340
|
+
const text = new Y.Text();
|
|
341
|
+
if (typeof value === "string" && value.length > 0) text.insert(0, value);
|
|
342
|
+
return text;
|
|
343
|
+
}
|
|
344
|
+
case "richtext": {
|
|
345
|
+
const text = new Y.Text();
|
|
346
|
+
if (typeof value === "string" && value.length > 0) text.insert(0, value);
|
|
347
|
+
else if (Array.isArray(value)) {
|
|
348
|
+
const delta = value.map((span) => {
|
|
349
|
+
const d = { insert: span.text };
|
|
350
|
+
if (span.marks && Object.keys(span.marks).length > 0) d.attributes = span.marks;
|
|
351
|
+
return d;
|
|
352
|
+
});
|
|
353
|
+
if (delta.length > 0) text.applyDelta(delta);
|
|
354
|
+
}
|
|
355
|
+
return text;
|
|
356
|
+
}
|
|
357
|
+
case "product":
|
|
358
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
|
|
359
|
+
return createStructuredMap(value, schema);
|
|
360
|
+
case "sequence": {
|
|
361
|
+
if (!Array.isArray(value)) return value;
|
|
362
|
+
const arr = new Y.Array();
|
|
363
|
+
const itemSchema = schema.item;
|
|
364
|
+
const items = value.map((item) => maybeCreateSharedType(item, itemSchema));
|
|
365
|
+
arr.insert(0, items);
|
|
366
|
+
return arr;
|
|
367
|
+
}
|
|
368
|
+
case "map": {
|
|
369
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
|
|
370
|
+
const map = new Y.Map();
|
|
371
|
+
const valueSchema = schema.item;
|
|
372
|
+
for (const [k, v] of Object.entries(value)) map.set(k, maybeCreateSharedType(v, valueSchema));
|
|
373
|
+
return map;
|
|
374
|
+
}
|
|
375
|
+
case "counter":
|
|
376
|
+
case "set":
|
|
377
|
+
case "tree":
|
|
378
|
+
case "movable": throw new Error(`Yjs substrate does not support [KIND]="${schema[KIND]}". This should have been caught at bind() time.`);
|
|
379
|
+
default: return value;
|
|
380
|
+
}
|
|
324
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Create a Y.Map from a plain object, recursively creating nested
|
|
384
|
+
* shared types as guided by the product schema.
|
|
385
|
+
*
|
|
386
|
+
* Follows populate-then-attach: fully populates the map before the
|
|
387
|
+
* caller inserts it into a parent container.
|
|
388
|
+
*/
|
|
325
389
|
function createStructuredMap(obj, productSchema) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (key in obj) continue;
|
|
343
|
-
if (fieldSchema[KIND2] === "text") {
|
|
344
|
-
map.set(key, new Y3.Text());
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return map;
|
|
348
|
-
}
|
|
349
|
-
function eventsToOps(events, schema) {
|
|
350
|
-
const ops = [];
|
|
351
|
-
for (const event of events) {
|
|
352
|
-
const kynetaPath = yjsPathToKynetaPath(event.path);
|
|
353
|
-
const change2 = eventToChange(event);
|
|
354
|
-
if (change2) {
|
|
355
|
-
ops.push({ path: kynetaPath, change: change2 });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return expandMapOpsToLeaves(ops, schema);
|
|
359
|
-
}
|
|
360
|
-
function yjsPathToKynetaPath(yjsPath) {
|
|
361
|
-
let path = RawPath.empty;
|
|
362
|
-
for (const segment of yjsPath) {
|
|
363
|
-
if (typeof segment === "string") {
|
|
364
|
-
path = path.field(segment);
|
|
365
|
-
} else if (typeof segment === "number") {
|
|
366
|
-
path = path.item(segment);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return path;
|
|
370
|
-
}
|
|
371
|
-
function eventToChange(event) {
|
|
372
|
-
if (event.target instanceof Y3.Text) {
|
|
373
|
-
return textEventToChange(event);
|
|
374
|
-
}
|
|
375
|
-
if (event.target instanceof Y3.Array) {
|
|
376
|
-
return arrayEventToChange(event);
|
|
377
|
-
}
|
|
378
|
-
if (event.target instanceof Y3.Map) {
|
|
379
|
-
return mapEventToChange(event);
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
390
|
+
const map = new Y.Map();
|
|
391
|
+
if (productSchema[KIND] !== "product") {
|
|
392
|
+
for (const [key, val] of Object.entries(obj)) map.set(key, val);
|
|
393
|
+
return map;
|
|
394
|
+
}
|
|
395
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
396
|
+
if (val === void 0) continue;
|
|
397
|
+
const fieldSchema = productSchema.fields[key];
|
|
398
|
+
const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val;
|
|
399
|
+
map.set(key, yjsVal);
|
|
400
|
+
}
|
|
401
|
+
for (const [key, fieldSchema] of Object.entries(productSchema.fields)) {
|
|
402
|
+
if (key in obj) continue;
|
|
403
|
+
if (fieldSchema[KIND] === "text" || fieldSchema[KIND] === "richtext") map.set(key, new Y.Text());
|
|
404
|
+
}
|
|
405
|
+
return map;
|
|
382
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Convert `observeDeep` events into kyneta `Op[]` for changefeed delivery.
|
|
409
|
+
*
|
|
410
|
+
* Each `Y.YEvent` in the array maps to one Op with:
|
|
411
|
+
* - `path`: derived from `event.path` (relative to the observed root Y.Map)
|
|
412
|
+
* - `change`: derived from the event's delta/keys based on target type
|
|
413
|
+
*
|
|
414
|
+
* `event.path` in `observeDeep` is relative to the observed shared type.
|
|
415
|
+
* Since we observe `rootMap` (the single root Y.Map), paths map directly
|
|
416
|
+
* to kyneta `PathSegment[]`.
|
|
417
|
+
*
|
|
418
|
+
* @param events - The events from the `observeDeep` callback
|
|
419
|
+
*/
|
|
420
|
+
function eventsToOps(events, schema, binding) {
|
|
421
|
+
const ops = [];
|
|
422
|
+
for (const event of events) {
|
|
423
|
+
const kynetaPath = yjsPathToKynetaPath(event.path, binding);
|
|
424
|
+
const change = eventToChange(event, schema, kynetaPath, binding);
|
|
425
|
+
if (change) ops.push({
|
|
426
|
+
path: kynetaPath,
|
|
427
|
+
change
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return expandMapOpsToLeaves(ops, schema);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Convert a Yjs event path (array of string | number) to a kyneta Path.
|
|
434
|
+
*
|
|
435
|
+
* `event.path` from `observeDeep` is relative to the observed type.
|
|
436
|
+
* Strings become key segments, numbers become index segments.
|
|
437
|
+
*/
|
|
438
|
+
function yjsPathToKynetaPath(yjsPath, binding) {
|
|
439
|
+
let path = RawPath.empty;
|
|
440
|
+
for (const segment of yjsPath) if (typeof segment === "string") {
|
|
441
|
+
const absPath = binding?.inverse.get(segment);
|
|
442
|
+
if (absPath) {
|
|
443
|
+
const lastDot = absPath.lastIndexOf(".");
|
|
444
|
+
const leaf = lastDot >= 0 ? absPath.slice(lastDot + 1) : absPath;
|
|
445
|
+
path = path.field(leaf);
|
|
446
|
+
} else path = path.field(segment);
|
|
447
|
+
} else if (typeof segment === "number") path = path.item(segment);
|
|
448
|
+
return path;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Convert a single Yjs event into a kyneta Change.
|
|
452
|
+
*
|
|
453
|
+
* For Y.Text events, dispatches to either `textEventToChange` or
|
|
454
|
+
* `richTextEventToChange` based on the schema at the event's path.
|
|
455
|
+
* Both text and richtext produce `Y.YTextEvent`, so schema awareness
|
|
456
|
+
* is required for correct dispatch.
|
|
457
|
+
*
|
|
458
|
+
* Returns null for event types we can't map.
|
|
459
|
+
*/
|
|
460
|
+
function eventToChange(event, rootSchema, kynetaPath, binding) {
|
|
461
|
+
if (event.target instanceof Y.Text) {
|
|
462
|
+
if (resolveSchemaAtPath(rootSchema, kynetaPath)[KIND] === "richtext") return richTextEventToChange(event);
|
|
463
|
+
return textEventToChange(event);
|
|
464
|
+
}
|
|
465
|
+
if (event.target instanceof Y.Array) return arrayEventToChange(event);
|
|
466
|
+
if (event.target instanceof Y.Map) return mapEventToChange(event, binding);
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Y.Text event → TextChange.
|
|
471
|
+
*
|
|
472
|
+
* `event.delta` uses the Quill Delta format, structurally identical to
|
|
473
|
+
* kyneta `TextInstruction[]`. We strip the `attributes` field (rich text
|
|
474
|
+
* formatting not surfaced by kyneta plain text).
|
|
475
|
+
*/
|
|
383
476
|
function textEventToChange(event) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
477
|
+
const instructions = [];
|
|
478
|
+
for (const delta of event.delta) if (delta.retain !== void 0) instructions.push({ retain: delta.retain });
|
|
479
|
+
else if (delta.insert !== void 0) instructions.push({ insert: delta.insert });
|
|
480
|
+
else if (delta.delete !== void 0) instructions.push({ delete: delta.delete });
|
|
481
|
+
return {
|
|
482
|
+
type: "text",
|
|
483
|
+
instructions
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Y.Text event → RichTextChange.
|
|
488
|
+
*
|
|
489
|
+
* `event.delta` uses the Quill Delta format. We map each delta op to a
|
|
490
|
+
* `RichTextInstruction`, preserving `attributes` as `marks` for format
|
|
491
|
+
* and insert instructions.
|
|
492
|
+
*/
|
|
493
|
+
function richTextEventToChange(event) {
|
|
494
|
+
const instructions = [];
|
|
495
|
+
for (const delta of event.delta) if (delta.retain !== void 0) {
|
|
496
|
+
const attrs = delta.attributes;
|
|
497
|
+
if (attrs && Object.keys(attrs).length > 0) instructions.push({
|
|
498
|
+
format: delta.retain,
|
|
499
|
+
marks: attrs
|
|
500
|
+
});
|
|
501
|
+
else instructions.push({ retain: delta.retain });
|
|
502
|
+
} else if (delta.insert !== void 0) {
|
|
503
|
+
const attrs = delta.attributes;
|
|
504
|
+
if (attrs && Object.keys(attrs).length > 0) instructions.push({
|
|
505
|
+
insert: delta.insert,
|
|
506
|
+
marks: attrs
|
|
507
|
+
});
|
|
508
|
+
else instructions.push({ insert: delta.insert });
|
|
509
|
+
} else if (delta.delete !== void 0) instructions.push({ delete: delta.delete });
|
|
510
|
+
return richTextChange(instructions);
|
|
395
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Y.Array event → SequenceChange.
|
|
514
|
+
*
|
|
515
|
+
* `event.changes.delta` provides the same cursor-based ops as kyneta
|
|
516
|
+
* SequenceInstruction[]. Container values (Y.Map, Y.Array) in insert
|
|
517
|
+
* arrays are converted to plain objects via `.toJSON()`.
|
|
518
|
+
*/
|
|
396
519
|
function arrayEventToChange(event) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return { type: "sequence", instructions };
|
|
411
|
-
}
|
|
412
|
-
function mapEventToChange(event) {
|
|
413
|
-
const set = {};
|
|
414
|
-
const deleteKeys = [];
|
|
415
|
-
let hasSet = false;
|
|
416
|
-
let hasDelete = false;
|
|
417
|
-
const target = event.target;
|
|
418
|
-
event.changes.keys.forEach((change2, key) => {
|
|
419
|
-
if (change2.action === "add" || change2.action === "update") {
|
|
420
|
-
const value = target.get(key);
|
|
421
|
-
set[key] = extractEventValue(value);
|
|
422
|
-
hasSet = true;
|
|
423
|
-
} else if (change2.action === "delete") {
|
|
424
|
-
deleteKeys.push(key);
|
|
425
|
-
hasDelete = true;
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
if (!hasSet && !hasDelete) return null;
|
|
429
|
-
return {
|
|
430
|
-
type: "map",
|
|
431
|
-
...hasSet ? { set } : {},
|
|
432
|
-
...hasDelete ? { delete: deleteKeys } : {}
|
|
433
|
-
};
|
|
520
|
+
const instructions = [];
|
|
521
|
+
for (const delta of event.changes.delta) if (delta.retain !== void 0) instructions.push({ retain: delta.retain });
|
|
522
|
+
else if (delta.delete !== void 0) instructions.push({ delete: delta.delete });
|
|
523
|
+
else if (delta.insert !== void 0) {
|
|
524
|
+
const items = delta.insert.map((item) => extractEventValue(item));
|
|
525
|
+
instructions.push({ insert: items });
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
type: "sequence",
|
|
529
|
+
instructions
|
|
530
|
+
};
|
|
434
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Y.Map event → MapChange.
|
|
534
|
+
*
|
|
535
|
+
* `event.changes.keys` is a `Map<string, { action: 'add'|'update'|'delete', ... }>`.
|
|
536
|
+
* - `action: 'add'|'update'` → `set[key] = map.get(key)`
|
|
537
|
+
* - `action: 'delete'` → `delete.push(key)`
|
|
538
|
+
*/
|
|
539
|
+
function mapEventToChange(event, binding) {
|
|
540
|
+
const set = {};
|
|
541
|
+
const deleteKeys = [];
|
|
542
|
+
let hasSet = false;
|
|
543
|
+
let hasDelete = false;
|
|
544
|
+
const target = event.target;
|
|
545
|
+
event.changes.keys.forEach((change, key) => {
|
|
546
|
+
const absPath = binding?.inverse.get(key);
|
|
547
|
+
const fieldName = absPath ? absPath.lastIndexOf(".") >= 0 ? absPath.slice(absPath.lastIndexOf(".") + 1) : absPath : key;
|
|
548
|
+
if (change.action === "add" || change.action === "update") {
|
|
549
|
+
set[fieldName] = extractEventValue(target.get(key));
|
|
550
|
+
hasSet = true;
|
|
551
|
+
} else if (change.action === "delete") {
|
|
552
|
+
deleteKeys.push(fieldName);
|
|
553
|
+
hasDelete = true;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
if (!hasSet && !hasDelete) return null;
|
|
557
|
+
return {
|
|
558
|
+
type: "map",
|
|
559
|
+
...hasSet ? { set } : {},
|
|
560
|
+
...hasDelete ? { delete: deleteKeys } : {}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Convert a Yjs value from an event into a plain value.
|
|
565
|
+
* Container values (Y.Map, Y.Array, Y.Text) → `.toJSON()`.
|
|
566
|
+
* Plain values → returned as-is.
|
|
567
|
+
*/
|
|
435
568
|
function extractEventValue(value) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
569
|
+
if (value instanceof Y.Map) return value.toJSON();
|
|
570
|
+
if (value instanceof Y.Array) return value.toJSON();
|
|
571
|
+
if (value instanceof Y.Text) return value.toJSON();
|
|
572
|
+
return value;
|
|
440
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Resolve the schema at a given path by walking through advanceSchema.
|
|
576
|
+
*/
|
|
441
577
|
function resolveSchemaAtPath(rootSchema, path) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
return schema;
|
|
578
|
+
let schema = rootSchema;
|
|
579
|
+
for (const seg of path.segments) schema = advanceSchema(schema, seg);
|
|
580
|
+
return schema;
|
|
447
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Get the item schema from a sequence schema, if available.
|
|
584
|
+
*/
|
|
448
585
|
function getItemSchema(schema) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return void 0;
|
|
586
|
+
if (schema[KIND] === "sequence") return schema.item;
|
|
587
|
+
if (schema[KIND] === "movable") return schema.item;
|
|
452
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Get the field schema from a product or map schema for a given key.
|
|
591
|
+
*/
|
|
453
592
|
function getFieldSchema(schema, key) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (schema[KIND2] === "map") {
|
|
458
|
-
return schema.item;
|
|
459
|
-
}
|
|
460
|
-
if (schema[KIND2] === "set") {
|
|
461
|
-
return schema.item;
|
|
462
|
-
}
|
|
463
|
-
return void 0;
|
|
593
|
+
if (schema[KIND] === "product") return schema.fields[key];
|
|
594
|
+
if (schema[KIND] === "map") return schema.item;
|
|
595
|
+
if (schema[KIND] === "set") return schema.item;
|
|
464
596
|
}
|
|
465
597
|
function pathToString(path) {
|
|
466
|
-
|
|
598
|
+
return path.segments.map((seg) => String(seg.resolve())).join(".");
|
|
467
599
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/position.ts
|
|
602
|
+
/** Map kyneta Side to Yjs assoc. Left → -1 (left-sticky), Right → 0 (right-sticky). */
|
|
603
|
+
function toYjsAssoc(side) {
|
|
604
|
+
return side === "left" ? -1 : 0;
|
|
605
|
+
}
|
|
606
|
+
/** Map Yjs assoc to kyneta Side. Negative → left, non-negative → right. */
|
|
607
|
+
function fromYjsAssoc(assoc) {
|
|
608
|
+
return assoc < 0 ? "left" : "right";
|
|
609
|
+
}
|
|
610
|
+
var YjsPosition = class {
|
|
611
|
+
side;
|
|
612
|
+
constructor(rpos, doc) {
|
|
613
|
+
this.rpos = rpos;
|
|
614
|
+
this.doc = doc;
|
|
615
|
+
this.side = fromYjsAssoc(rpos.assoc);
|
|
616
|
+
}
|
|
617
|
+
resolve() {
|
|
618
|
+
const abs = Y.createAbsolutePositionFromRelativePosition(this.rpos, this.doc);
|
|
619
|
+
return abs ? abs.index : null;
|
|
620
|
+
}
|
|
621
|
+
encode() {
|
|
622
|
+
return Y.encodeRelativePosition(this.rpos);
|
|
623
|
+
}
|
|
624
|
+
transform(_instructions) {}
|
|
625
|
+
};
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/reader.ts
|
|
628
|
+
/**
|
|
629
|
+
* Extract a plain value from a Yjs shared type or return a plain value as-is.
|
|
630
|
+
*
|
|
631
|
+
* - Y.Text → `.toJSON()` (string)
|
|
632
|
+
* - Y.Map → `.toJSON()` (plain object snapshot — for product/map reads)
|
|
633
|
+
* - Y.Array → `.toJSON()` (plain array snapshot)
|
|
634
|
+
* - Plain values (string, number, boolean, null) → returned as-is
|
|
635
|
+
*/
|
|
471
636
|
function extractValue(resolved) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return resolved.toJSON();
|
|
477
|
-
}
|
|
478
|
-
if (resolved instanceof Y4.Array) {
|
|
479
|
-
return resolved.toJSON();
|
|
480
|
-
}
|
|
481
|
-
return resolved;
|
|
482
|
-
}
|
|
483
|
-
function yjsReader(doc, schema) {
|
|
484
|
-
const rootMap = doc.getMap("root");
|
|
485
|
-
return {
|
|
486
|
-
read(path) {
|
|
487
|
-
if (path.length === 0) {
|
|
488
|
-
return rootMap.toJSON();
|
|
489
|
-
}
|
|
490
|
-
const resolved = resolveYjsType(rootMap, schema, path);
|
|
491
|
-
return extractValue(resolved);
|
|
492
|
-
},
|
|
493
|
-
arrayLength(path) {
|
|
494
|
-
const resolved = resolveYjsType(rootMap, schema, path);
|
|
495
|
-
if (resolved instanceof Y4.Array) {
|
|
496
|
-
return resolved.length;
|
|
497
|
-
}
|
|
498
|
-
if (Array.isArray(resolved)) {
|
|
499
|
-
return resolved.length;
|
|
500
|
-
}
|
|
501
|
-
return 0;
|
|
502
|
-
},
|
|
503
|
-
keys(path) {
|
|
504
|
-
const resolved = resolveYjsType(rootMap, schema, path);
|
|
505
|
-
if (resolved instanceof Y4.Map) {
|
|
506
|
-
return Array.from(resolved.keys());
|
|
507
|
-
}
|
|
508
|
-
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
509
|
-
return Object.keys(resolved);
|
|
510
|
-
}
|
|
511
|
-
return [];
|
|
512
|
-
},
|
|
513
|
-
hasKey(path, key) {
|
|
514
|
-
const resolved = resolveYjsType(rootMap, schema, path);
|
|
515
|
-
if (resolved instanceof Y4.Map) {
|
|
516
|
-
return resolved.has(key);
|
|
517
|
-
}
|
|
518
|
-
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
519
|
-
return key in resolved;
|
|
520
|
-
}
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
};
|
|
637
|
+
if (resolved instanceof Y.Text) return resolved.toJSON();
|
|
638
|
+
if (resolved instanceof Y.Map) return resolved.toJSON();
|
|
639
|
+
if (resolved instanceof Y.Array) return resolved.toJSON();
|
|
640
|
+
return resolved;
|
|
524
641
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Convert a Y.Text's delta (Quill format) to a kyneta RichTextDelta.
|
|
644
|
+
*
|
|
645
|
+
* Yjs `.toDelta()` returns `{ insert: string, attributes?: Record<string, any> }[]`.
|
|
646
|
+
* Kyneta RichTextDelta is `{ text: string, marks?: MarkMap }[]`.
|
|
647
|
+
*/
|
|
648
|
+
function yTextToRichTextDelta(ytext) {
|
|
649
|
+
const delta = ytext.toDelta();
|
|
650
|
+
const spans = [];
|
|
651
|
+
for (const d of delta) {
|
|
652
|
+
if (typeof d.insert !== "string") continue;
|
|
653
|
+
const span = d.attributes && Object.keys(d.attributes).length > 0 ? {
|
|
654
|
+
text: d.insert,
|
|
655
|
+
marks: d.attributes
|
|
656
|
+
} : { text: d.insert };
|
|
657
|
+
spans.push(span);
|
|
658
|
+
}
|
|
659
|
+
return spans;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Creates a Reader that navigates the Yjs shared type tree live,
|
|
663
|
+
* using the schema as a type witness to determine navigation at each
|
|
664
|
+
* path segment.
|
|
665
|
+
*
|
|
666
|
+
* The reader is a live view — mutations to the underlying Y.Doc
|
|
667
|
+
* (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
|
|
668
|
+
* visible through the reader.
|
|
669
|
+
*
|
|
670
|
+
* Internally obtains the root map via `doc.getMap("root")`.
|
|
671
|
+
*
|
|
672
|
+
* @param doc - The Y.Doc to read from.
|
|
673
|
+
* @param schema - The root schema for the document.
|
|
674
|
+
* @param binding - Optional SchemaBinding for identity-keyed navigation.
|
|
675
|
+
*/
|
|
676
|
+
function yjsReader(doc, schema, binding) {
|
|
677
|
+
const rootMap = doc.getMap("root");
|
|
678
|
+
return {
|
|
679
|
+
read(path) {
|
|
680
|
+
if (path.length === 0) return rootMap.toJSON();
|
|
681
|
+
const { resolved, schema: nodeSchema } = resolveYjsType(rootMap, schema, path, binding);
|
|
682
|
+
if (nodeSchema[KIND] === "richtext" && resolved instanceof Y.Text) return yTextToRichTextDelta(resolved);
|
|
683
|
+
return extractValue(resolved);
|
|
684
|
+
},
|
|
685
|
+
arrayLength(path) {
|
|
686
|
+
const { resolved } = resolveYjsType(rootMap, schema, path, binding);
|
|
687
|
+
if (resolved instanceof Y.Array) return resolved.length;
|
|
688
|
+
if (Array.isArray(resolved)) return resolved.length;
|
|
689
|
+
return 0;
|
|
690
|
+
},
|
|
691
|
+
keys(path) {
|
|
692
|
+
const { resolved } = resolveYjsType(rootMap, schema, path, binding);
|
|
693
|
+
if (resolved instanceof Y.Map) return Array.from(resolved.keys());
|
|
694
|
+
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) return Object.keys(resolved);
|
|
695
|
+
return [];
|
|
696
|
+
},
|
|
697
|
+
hasKey(path, key) {
|
|
698
|
+
const { resolved } = resolveYjsType(rootMap, schema, path, binding);
|
|
699
|
+
if (resolved instanceof Y.Map) return resolved.has(key);
|
|
700
|
+
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) return key in resolved;
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/version.ts
|
|
707
|
+
/**
|
|
708
|
+
* Encode a state vector map to Yjs's binary state vector format.
|
|
709
|
+
*
|
|
710
|
+
* Yjs does not export `encodeStateVector(map)` — only `Y.encodeStateVector(doc)`
|
|
711
|
+
* which requires a full doc. This implements the same binary format directly:
|
|
712
|
+
* `[entryCount: varint, (clientId: varint, clock: varint)*]`
|
|
713
|
+
*
|
|
714
|
+
* Each value is encoded as an unsigned LEB128 varint.
|
|
715
|
+
*/
|
|
716
|
+
function encodeStateVector$1(map) {
|
|
717
|
+
const bytes = [];
|
|
718
|
+
function writeVarUint(value) {
|
|
719
|
+
while (value > 127) {
|
|
720
|
+
bytes.push(value & 127 | 128);
|
|
721
|
+
value >>>= 7;
|
|
722
|
+
}
|
|
723
|
+
bytes.push(value & 127);
|
|
724
|
+
}
|
|
725
|
+
writeVarUint(map.size);
|
|
726
|
+
for (const [clientId, clock] of map) {
|
|
727
|
+
writeVarUint(clientId);
|
|
728
|
+
writeVarUint(clock);
|
|
729
|
+
}
|
|
730
|
+
return new Uint8Array(bytes);
|
|
731
|
+
}
|
|
732
|
+
function arraysEqual(a, b) {
|
|
733
|
+
if (a.length !== b.length) return false;
|
|
734
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* A Version wrapping a Yjs snapshot (state vector + delete set).
|
|
739
|
+
*
|
|
740
|
+
* The state vector tracks which insertions from each client have been
|
|
741
|
+
* observed. The delete set tracks which of those items have been
|
|
742
|
+
* tombstoned. Together they fully describe a Yjs document's state.
|
|
743
|
+
*
|
|
744
|
+
* - `sv` is used by `exportSince()` to compute the minimal update payload
|
|
745
|
+
* via `Y.encodeStateAsUpdate(doc, sv)`.
|
|
746
|
+
* - `snapshotBytes` is the encoded Yjs `Snapshot` (SV + delete set),
|
|
747
|
+
* used for equality comparison: two documents are "equal" only when
|
|
748
|
+
* both their inserts and deletes match.
|
|
749
|
+
*
|
|
750
|
+
* `compare()` first performs standard version-vector partial-order
|
|
751
|
+
* comparison on the state vectors. If the SVs are equal, it falls
|
|
752
|
+
* through to a byte-level comparison of the snapshot bytes — if they
|
|
753
|
+
* differ (same inserts, different deletes), the result is "concurrent",
|
|
754
|
+
* ensuring the sync protocol pushes the divergent deletes.
|
|
755
|
+
*/
|
|
756
|
+
var YjsVersion = class YjsVersion {
|
|
757
|
+
/** Encoded state vector — used by exportSince(). */
|
|
758
|
+
sv;
|
|
759
|
+
/**
|
|
760
|
+
* Encoded Yjs snapshot (state vector + delete set) — used for equality
|
|
761
|
+
* comparison. Two documents are "equal" only if both their inserts
|
|
762
|
+
* (state vector) and deletes (delete set) match.
|
|
763
|
+
*/
|
|
764
|
+
snapshotBytes;
|
|
765
|
+
constructor(sv, snapshotBytes) {
|
|
766
|
+
this.sv = sv;
|
|
767
|
+
this.snapshotBytes = snapshotBytes ?? sv;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Construct a version from a live `Y.Doc` by snapshotting its full state.
|
|
771
|
+
*
|
|
772
|
+
* Walks the struct store to derive the delete set — O(n) in the number
|
|
773
|
+
* of items. Use {@link fromDeleteSet} for the incremental path.
|
|
774
|
+
*/
|
|
775
|
+
static fromDoc(doc) {
|
|
776
|
+
return new YjsVersion(encodeStateVector(doc), encodeSnapshot(snapshot(doc)));
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Construct a version from a `Y.Doc`'s state vector and an externally
|
|
780
|
+
* maintained delete set — the incremental path that avoids a struct
|
|
781
|
+
* store walk.
|
|
782
|
+
*
|
|
783
|
+
* @param doc The live Y.Doc (for the state vector).
|
|
784
|
+
* @param ds An accumulated delete set, kept in sync by merging
|
|
785
|
+
* `transaction.deleteSet` on each transaction.
|
|
786
|
+
*/
|
|
787
|
+
static fromDeleteSet(doc, ds) {
|
|
788
|
+
const sv = encodeStateVector(doc);
|
|
789
|
+
return new YjsVersion(sv, encodeSnapshot(createSnapshot(ds, decodeStateVector(sv))));
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Serialize to a text-safe string.
|
|
793
|
+
*
|
|
794
|
+
* Format: `base64(sv) + "." + base64(snapshotBytes)`.
|
|
795
|
+
* The "." separator is unambiguous since base64 never contains ".".
|
|
796
|
+
*/
|
|
797
|
+
serialize() {
|
|
798
|
+
const svB64 = uint8ArrayToBase64(this.sv);
|
|
799
|
+
const snapB64 = uint8ArrayToBase64(this.snapshotBytes);
|
|
800
|
+
return svB64 + "." + snapB64;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Compare with another version using version-vector partial order,
|
|
804
|
+
* extended with delete-set equality checking.
|
|
805
|
+
*
|
|
806
|
+
* 1. Decode both state vectors and compare via `versionVectorCompare`.
|
|
807
|
+
* 2. If the SV comparison yields anything other than "equal", return it.
|
|
808
|
+
* 3. If the SVs are equal, compare snapshot bytes for byte equality.
|
|
809
|
+
* If they differ (same inserts, different deletes), return "concurrent"
|
|
810
|
+
* — both sides may have tombstones the other lacks.
|
|
811
|
+
* 4. "equal" is returned only when BOTH the state vector AND the
|
|
812
|
+
* delete set match.
|
|
813
|
+
*
|
|
814
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
815
|
+
*/
|
|
816
|
+
compare(other) {
|
|
817
|
+
if (!(other instanceof YjsVersion)) throw new Error("YjsVersion can only be compared with another YjsVersion");
|
|
818
|
+
const svResult = versionVectorCompare(decodeStateVector(this.sv), decodeStateVector(other.sv));
|
|
819
|
+
if (svResult !== "equal") return svResult;
|
|
820
|
+
return arraysEqual(this.snapshotBytes, other.snapshotBytes) ? "equal" : "concurrent";
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Greatest lower bound (lattice meet) of two Yjs versions.
|
|
824
|
+
*
|
|
825
|
+
* Decodes both state vectors, computes the component-wise minimum
|
|
826
|
+
* via `versionVectorMeet`, and encodes the result back to a Yjs
|
|
827
|
+
* state vector.
|
|
828
|
+
*
|
|
829
|
+
* The meet snapshot uses the meet SV with no delete-set information
|
|
830
|
+
* (conservative lower bound). meet() feeds into advance(), which Yjs
|
|
831
|
+
* does not support incrementally, so this is safe.
|
|
832
|
+
*
|
|
833
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
834
|
+
*/
|
|
835
|
+
meet(other) {
|
|
836
|
+
if (!(other instanceof YjsVersion)) throw new Error("YjsVersion can only be meet'd with another YjsVersion");
|
|
837
|
+
return new YjsVersion(encodeStateVector$1(versionVectorMeet(decodeStateVector(this.sv), decodeStateVector(other.sv))));
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Parse a serialized YjsVersion string back into a YjsVersion.
|
|
841
|
+
*
|
|
842
|
+
* New format: `base64(sv) + "." + base64(snapshotBytes)`.
|
|
843
|
+
* Legacy format (no "."): `base64(sv)` only — constructed with
|
|
844
|
+
* `snapshotBytes` equal to the SV bytes. When compared against a
|
|
845
|
+
* new-format version with matching SVs, the differing snapshot bytes
|
|
846
|
+
* yield "concurrent", triggering a safe redundant sync push.
|
|
847
|
+
*/
|
|
848
|
+
static parse(serialized) {
|
|
849
|
+
if (serialized === "") throw new Error("Invalid YjsVersion value: (empty string)");
|
|
850
|
+
const dotIndex = serialized.indexOf(".");
|
|
851
|
+
if (dotIndex === -1) return new YjsVersion(base64ToUint8Array(serialized));
|
|
852
|
+
return new YjsVersion(base64ToUint8Array(serialized.slice(0, dotIndex)), base64ToUint8Array(serialized.slice(dotIndex + 1)));
|
|
853
|
+
}
|
|
611
854
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/substrate.ts
|
|
857
|
+
const KYNETA_ORIGIN = "kyneta-prepare";
|
|
858
|
+
/**
|
|
859
|
+
* Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
|
|
860
|
+
*
|
|
861
|
+
* This is the "bring your own doc" entry point. The user creates and
|
|
862
|
+
* manages the Y.Doc (possibly via a Yjs provider); this function wraps
|
|
863
|
+
* it with a schema-aware overlay providing typed reads, writes,
|
|
864
|
+
* versioning, and export/merge through the standard Substrate interface.
|
|
865
|
+
*
|
|
866
|
+
* **Event bridge contract:** A persistent `observeDeep` handler is
|
|
867
|
+
* registered on the root Y.Map at construction time. All non-kyneta
|
|
868
|
+
* mutations to the Y.Doc (merges, external local writes) are bridged
|
|
869
|
+
* to the kyneta changefeed. Subscribing to the kyneta doc observes all
|
|
870
|
+
* mutations regardless of source.
|
|
871
|
+
*
|
|
872
|
+
* @param doc - The Y.Doc to wrap. The substrate does NOT own the doc;
|
|
873
|
+
* the caller is responsible for its lifecycle.
|
|
874
|
+
* @param schema - The root schema for the document.
|
|
875
|
+
* @param binding - Optional SchemaBinding for identity-keyed containers.
|
|
876
|
+
*/
|
|
877
|
+
function createYjsSubstrate(doc, schema, binding) {
|
|
878
|
+
const pendingChanges = [];
|
|
879
|
+
let inOurTransaction = false;
|
|
880
|
+
let pendingMergeOrigin;
|
|
881
|
+
let cachedCtx;
|
|
882
|
+
let accumulatedDs = Y.createDeleteSetFromStructStore(doc.store);
|
|
883
|
+
const rootMap = doc.getMap("root");
|
|
884
|
+
const reader = yjsReader(doc, schema, binding);
|
|
885
|
+
const substrate = {
|
|
886
|
+
[BACKING_DOC]: doc,
|
|
887
|
+
reader,
|
|
888
|
+
prepare(path, change) {
|
|
889
|
+
if (!inOurTransaction) pendingChanges.push({
|
|
890
|
+
path,
|
|
891
|
+
change
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
onFlush(_origin) {
|
|
895
|
+
if (!inOurTransaction && pendingChanges.length > 0) {
|
|
896
|
+
inOurTransaction = true;
|
|
897
|
+
try {
|
|
898
|
+
doc.transact(() => {
|
|
899
|
+
for (const { path, change } of pendingChanges) applyChangeToYjs(rootMap, schema, path, change, binding);
|
|
900
|
+
}, KYNETA_ORIGIN);
|
|
901
|
+
pendingChanges.length = 0;
|
|
902
|
+
} finally {
|
|
903
|
+
inOurTransaction = false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
context() {
|
|
908
|
+
if (!cachedCtx) {
|
|
909
|
+
cachedCtx = buildWritableContext(substrate);
|
|
910
|
+
cachedCtx.nativeResolver = (nodeSchema, path) => {
|
|
911
|
+
if (path.segments.length === 0) return doc;
|
|
912
|
+
if (nodeSchema[KIND] === "scalar" || nodeSchema[KIND] === "sum") return void 0;
|
|
913
|
+
return resolveYjsType(rootMap, schema, path, binding).resolved;
|
|
914
|
+
};
|
|
915
|
+
cachedCtx.positionResolver = (_nodeSchema, path) => {
|
|
916
|
+
return {
|
|
917
|
+
createPosition(index, side) {
|
|
918
|
+
const { resolved: ytype } = resolveYjsType(rootMap, schema, path, binding);
|
|
919
|
+
if (!(ytype instanceof Y.Text)) throw new Error(`positionResolver: path does not resolve to a Y.Text`);
|
|
920
|
+
const assoc = toYjsAssoc(side);
|
|
921
|
+
return new YjsPosition(Y.createRelativePositionFromTypeIndex(ytype, index, assoc), doc);
|
|
922
|
+
},
|
|
923
|
+
decodePosition(bytes) {
|
|
924
|
+
return new YjsPosition(Y.decodeRelativePosition(bytes), doc);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return cachedCtx;
|
|
930
|
+
},
|
|
931
|
+
version() {
|
|
932
|
+
return YjsVersion.fromDeleteSet(doc, accumulatedDs);
|
|
933
|
+
},
|
|
934
|
+
baseVersion() {
|
|
935
|
+
return new YjsVersion(new Uint8Array([0]));
|
|
936
|
+
},
|
|
937
|
+
advance(_to) {
|
|
938
|
+
throw new Error("advance() on a live Yjs substrate is not yet supported. Use advance() on a YjsReplica instead.");
|
|
939
|
+
},
|
|
940
|
+
exportEntirety() {
|
|
941
|
+
return {
|
|
942
|
+
kind: "entirety",
|
|
943
|
+
encoding: "binary",
|
|
944
|
+
data: Y.encodeStateAsUpdate(doc)
|
|
945
|
+
};
|
|
946
|
+
},
|
|
947
|
+
exportSince(since) {
|
|
948
|
+
try {
|
|
949
|
+
return {
|
|
950
|
+
kind: "since",
|
|
951
|
+
encoding: "binary",
|
|
952
|
+
data: Y.encodeStateAsUpdate(doc, since.sv)
|
|
953
|
+
};
|
|
954
|
+
} catch {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
merge(payload, origin) {
|
|
959
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) throw new Error("YjsSubstrate.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data.");
|
|
960
|
+
pendingMergeOrigin = origin;
|
|
961
|
+
try {
|
|
962
|
+
Y.applyUpdate(doc, payload.data, origin ?? "remote");
|
|
963
|
+
} finally {
|
|
964
|
+
pendingMergeOrigin = void 0;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
rootMap.observeDeep((events, transaction) => {
|
|
969
|
+
if (transaction.origin === KYNETA_ORIGIN) return;
|
|
970
|
+
const ops = eventsToOps(events, schema, binding);
|
|
971
|
+
if (ops.length === 0) return;
|
|
972
|
+
if (transaction.deleteSet.clients.size > 0) accumulatedDs = Y.mergeDeleteSets([accumulatedDs, transaction.deleteSet]);
|
|
973
|
+
const origin = pendingMergeOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
|
|
974
|
+
const ctx = substrate.context();
|
|
975
|
+
inOurTransaction = true;
|
|
976
|
+
try {
|
|
977
|
+
executeBatch(ctx, ops, origin);
|
|
978
|
+
} finally {
|
|
979
|
+
inOurTransaction = false;
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
doc.on("afterTransaction", (transaction) => {
|
|
983
|
+
if (transaction.origin === KYNETA_ORIGIN && transaction.deleteSet.clients.size > 0) accumulatedDs = Y.mergeDeleteSets([accumulatedDs, transaction.deleteSet]);
|
|
984
|
+
});
|
|
985
|
+
return substrate;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Factory for constructing Yjs-backed substrates.
|
|
989
|
+
*
|
|
990
|
+
* - `create(schema)` — creates a fresh Y.Doc with empty containers
|
|
991
|
+
* matching the schema structure. No seed data — initial content
|
|
992
|
+
* should be applied via `change()` after construction.
|
|
993
|
+
* - `fromEntirety(payload, schema)` — creates a Y.Doc from an entirety
|
|
994
|
+
* payload, returns a substrate.
|
|
995
|
+
* - `parseVersion(serialized)` — deserializes a YjsVersion.
|
|
996
|
+
*
|
|
997
|
+
* Uses trivialBinding for identity-keying: every path maps to
|
|
998
|
+
* `deriveIdentity(path, 1)` (generation 1, no renames).
|
|
999
|
+
*/
|
|
1000
|
+
/**
|
|
1001
|
+
* Compute a trivial SchemaBinding for a schema with no migration history.
|
|
1002
|
+
* Every product field maps to `deriveIdentity(path, 1)`.
|
|
1003
|
+
*/
|
|
1004
|
+
function trivialBinding(schema) {
|
|
1005
|
+
if (schema[KIND] === "product") return deriveSchemaBinding(schema, {});
|
|
1006
|
+
return {
|
|
1007
|
+
forward: /* @__PURE__ */ new Map(),
|
|
1008
|
+
inverse: /* @__PURE__ */ new Map()
|
|
1009
|
+
};
|
|
715
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Schema-free replica factory for Yjs substrates.
|
|
1013
|
+
*
|
|
1014
|
+
* Constructs headless `Replica<YjsVersion>` instances backed by bare
|
|
1015
|
+
* `Y.Doc`s — no schema walking, no container initialization, no
|
|
1016
|
+
* Reader, no event bridge, no changefeed. Just the CRDT runtime
|
|
1017
|
+
* with version tracking and export/merge.
|
|
1018
|
+
*
|
|
1019
|
+
* Used by conduit participants (stores, routing servers)
|
|
1020
|
+
* that need to accumulate state, compute per-peer deltas, and compact
|
|
1021
|
+
* storage without ever interpreting document fields.
|
|
1022
|
+
*/
|
|
716
1023
|
function createYjsReplica(doc) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
Y5.applyUpdate(doc, payload.data);
|
|
783
|
-
return createYjsReplica(doc);
|
|
784
|
-
},
|
|
785
|
-
parseVersion(serialized) {
|
|
786
|
-
return YjsVersion.parse(serialized);
|
|
787
|
-
}
|
|
1024
|
+
let currentDoc = doc;
|
|
1025
|
+
let currentBase = new YjsVersion(Y.encodeStateVector(new Y.Doc()));
|
|
1026
|
+
return {
|
|
1027
|
+
get [BACKING_DOC]() {
|
|
1028
|
+
return currentDoc;
|
|
1029
|
+
},
|
|
1030
|
+
version() {
|
|
1031
|
+
return YjsVersion.fromDoc(currentDoc);
|
|
1032
|
+
},
|
|
1033
|
+
baseVersion() {
|
|
1034
|
+
return currentBase;
|
|
1035
|
+
},
|
|
1036
|
+
advance(to) {
|
|
1037
|
+
if (currentBase.compare(to) === "ahead") throw new Error("advance(): target is behind base version");
|
|
1038
|
+
const currentCmp = to.compare(this.version());
|
|
1039
|
+
if (currentCmp === "ahead") throw new Error("advance(): target is ahead of current version");
|
|
1040
|
+
if (currentCmp !== "equal") return;
|
|
1041
|
+
const update = Y.encodeStateAsUpdate(currentDoc);
|
|
1042
|
+
const newDoc = new Y.Doc();
|
|
1043
|
+
Y.applyUpdate(newDoc, update);
|
|
1044
|
+
currentDoc = newDoc;
|
|
1045
|
+
currentBase = YjsVersion.fromDoc(currentDoc);
|
|
1046
|
+
},
|
|
1047
|
+
exportEntirety() {
|
|
1048
|
+
return {
|
|
1049
|
+
kind: "entirety",
|
|
1050
|
+
encoding: "binary",
|
|
1051
|
+
data: Y.encodeStateAsUpdate(currentDoc)
|
|
1052
|
+
};
|
|
1053
|
+
},
|
|
1054
|
+
exportSince(since) {
|
|
1055
|
+
try {
|
|
1056
|
+
return {
|
|
1057
|
+
kind: "since",
|
|
1058
|
+
encoding: "binary",
|
|
1059
|
+
data: Y.encodeStateAsUpdate(currentDoc, since.sv)
|
|
1060
|
+
};
|
|
1061
|
+
} catch {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
merge(payload, _origin) {
|
|
1066
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) throw new Error("YjsReplica.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data.");
|
|
1067
|
+
Y.applyUpdate(currentDoc, payload.data);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
const yjsReplicaFactory = {
|
|
1072
|
+
replicaType: [
|
|
1073
|
+
"yjs",
|
|
1074
|
+
1,
|
|
1075
|
+
0
|
|
1076
|
+
],
|
|
1077
|
+
createEmpty() {
|
|
1078
|
+
return createYjsReplica(new Y.Doc());
|
|
1079
|
+
},
|
|
1080
|
+
fromEntirety(payload) {
|
|
1081
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) throw new Error("YjsReplicaFactory.fromEntirety only supports binary-encoded payloads");
|
|
1082
|
+
const doc = new Y.Doc();
|
|
1083
|
+
Y.applyUpdate(doc, payload.data);
|
|
1084
|
+
return createYjsReplica(doc);
|
|
1085
|
+
},
|
|
1086
|
+
parseVersion(serialized) {
|
|
1087
|
+
return YjsVersion.parse(serialized);
|
|
1088
|
+
}
|
|
788
1089
|
};
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1090
|
+
const yjsSubstrateFactory = {
|
|
1091
|
+
replica: yjsReplicaFactory,
|
|
1092
|
+
createReplica() {
|
|
1093
|
+
return createYjsReplica(new Y.Doc());
|
|
1094
|
+
},
|
|
1095
|
+
upgrade(replica, schema) {
|
|
1096
|
+
const doc = replica[BACKING_DOC];
|
|
1097
|
+
const binding = trivialBinding(schema);
|
|
1098
|
+
ensureContainers(doc, schema, true, binding);
|
|
1099
|
+
return createYjsSubstrate(doc, schema, binding);
|
|
1100
|
+
},
|
|
1101
|
+
create(schema) {
|
|
1102
|
+
const doc = new Y.Doc();
|
|
1103
|
+
const binding = trivialBinding(schema);
|
|
1104
|
+
ensureContainers(doc, schema, false, binding);
|
|
1105
|
+
return createYjsSubstrate(doc, schema, binding);
|
|
1106
|
+
},
|
|
1107
|
+
fromEntirety(payload, schema) {
|
|
1108
|
+
const replica = this.createReplica();
|
|
1109
|
+
replica.merge(payload);
|
|
1110
|
+
return this.upgrade(replica, schema);
|
|
1111
|
+
},
|
|
1112
|
+
parseVersion(serialized) {
|
|
1113
|
+
return YjsVersion.parse(serialized);
|
|
1114
|
+
}
|
|
812
1115
|
};
|
|
813
|
-
|
|
814
|
-
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region src/bind-yjs.ts
|
|
1118
|
+
/**
|
|
1119
|
+
* Hash a string peerId to a deterministic numeric Yjs clientID.
|
|
1120
|
+
*
|
|
1121
|
+
* Yjs clientIDs are unsigned 32-bit integers. We use FNV-1a hash to
|
|
1122
|
+
* produce a deterministic uint32 from the string peerId.
|
|
1123
|
+
*
|
|
1124
|
+
* The hash is deterministic: the same string always produces the same
|
|
1125
|
+
* numeric clientID, across restarts and across machines.
|
|
1126
|
+
*/
|
|
815
1127
|
function hashPeerId(peerId) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1128
|
+
let hash = 2166136261;
|
|
1129
|
+
for (let i = 0; i < peerId.length; i++) {
|
|
1130
|
+
hash ^= peerId.charCodeAt(i);
|
|
1131
|
+
hash = Math.imul(hash, 16777619);
|
|
1132
|
+
}
|
|
1133
|
+
const result = hash >>> 0;
|
|
1134
|
+
return result === STRUCTURAL_YJS_CLIENT_ID ? 1 : result;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Create a SubstrateFactory<YjsVersion> that sets doc.clientID
|
|
1138
|
+
* on every new Y.Doc with a deterministic uint32 clientID derived
|
|
1139
|
+
* from the exchange's string peerId.
|
|
1140
|
+
*/
|
|
1141
|
+
function createYjsFactory(peerId, binding) {
|
|
1142
|
+
const numericClientId = hashPeerId(peerId);
|
|
1143
|
+
return {
|
|
1144
|
+
replica: yjsReplicaFactory,
|
|
1145
|
+
createReplica() {
|
|
1146
|
+
return createYjsReplica(new Y.Doc());
|
|
1147
|
+
},
|
|
1148
|
+
upgrade(replica, schema) {
|
|
1149
|
+
const doc = replica[BACKING_DOC];
|
|
1150
|
+
doc.clientID = numericClientId;
|
|
1151
|
+
ensureContainers(doc, schema, true, binding);
|
|
1152
|
+
return createYjsSubstrate(doc, schema, binding);
|
|
1153
|
+
},
|
|
1154
|
+
create(schema) {
|
|
1155
|
+
const doc = new Y.Doc();
|
|
1156
|
+
doc.clientID = numericClientId;
|
|
1157
|
+
ensureContainers(doc, schema, false, binding);
|
|
1158
|
+
return createYjsSubstrate(doc, schema, binding);
|
|
1159
|
+
},
|
|
1160
|
+
fromEntirety(payload, schema) {
|
|
1161
|
+
const replica = this.createReplica();
|
|
1162
|
+
replica.merge(payload);
|
|
1163
|
+
return this.upgrade(replica, schema);
|
|
1164
|
+
},
|
|
1165
|
+
parseVersion(serialized) {
|
|
1166
|
+
return YjsVersion.parse(serialized);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* The Yjs CRDT binding target.
|
|
1172
|
+
*
|
|
1173
|
+
* - `yjs.bind(schema)` — bind a schema to Yjs with collaborative sync
|
|
1174
|
+
* - `yjs.replica()` — create a collaborative replica
|
|
1175
|
+
*
|
|
1176
|
+
* Laws are constrained to `YjsLaws` — schemas requiring composition laws
|
|
1177
|
+
* outside this set (e.g. `"additive"` from `Schema.counter()`,
|
|
1178
|
+
* `"positional-ot-move"` from `Schema.movableList()`) are rejected at
|
|
1179
|
+
* compile time.
|
|
1180
|
+
*
|
|
1181
|
+
* To access the underlying Y.Doc, use `unwrap(ref)` from `@kyneta/schema`.
|
|
1182
|
+
*/
|
|
1183
|
+
const yjs = createBindingTarget({
|
|
1184
|
+
factory: (ctx) => createYjsFactory(ctx.peerId, ctx.binding),
|
|
1185
|
+
replicaFactory: yjsReplicaFactory,
|
|
1186
|
+
syncProtocol: SYNC_COLLABORATIVE
|
|
865
1187
|
});
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
YjsVersion,
|
|
870
|
-
applyChangeToYjs,
|
|
871
|
-
applyChanges,
|
|
872
|
-
change,
|
|
873
|
-
createDoc,
|
|
874
|
-
createRef,
|
|
875
|
-
createYjsSubstrate,
|
|
876
|
-
ensureContainers,
|
|
877
|
-
eventsToOps,
|
|
878
|
-
exportEntirety,
|
|
879
|
-
exportSince,
|
|
880
|
-
merge,
|
|
881
|
-
resolveYjsType,
|
|
882
|
-
stepIntoYjs,
|
|
883
|
-
subscribe,
|
|
884
|
-
subscribeNode,
|
|
885
|
-
unwrap,
|
|
886
|
-
version,
|
|
887
|
-
yjs,
|
|
888
|
-
yjsReader,
|
|
889
|
-
yjsReplicaFactory,
|
|
890
|
-
yjsSubstrateFactory
|
|
891
|
-
};
|
|
1188
|
+
//#endregion
|
|
1189
|
+
export { NATIVE, Schema, YjsPosition, YjsVersion, applyChangeToYjs, applyChanges, change, createDoc, createRef, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, fromYjsAssoc, merge, resolveYjsType, stepIntoYjs, subscribe, subscribeNode, toYjsAssoc, unwrap, version, yjs, yjsReader, yjsReplicaFactory, yjsSubstrateFactory };
|
|
1190
|
+
|
|
892
1191
|
//# sourceMappingURL=index.js.map
|