@kyneta/yjs-schema 1.1.0 → 1.3.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 +3 -3
- package/dist/index.d.ts +97 -225
- package/dist/index.js +281 -316
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +325 -0
- package/src/__tests__/bind-yjs.test.ts +79 -70
- package/src/__tests__/create.test.ts +88 -65
- package/src/__tests__/reader.test.ts +38 -72
- package/src/__tests__/record-text-spike.test.ts +47 -46
- package/src/__tests__/structural-merge.test.ts +18 -18
- package/src/__tests__/substrate.test.ts +62 -58
- package/src/__tests__/version.test.ts +75 -0
- package/src/bind-yjs.ts +40 -41
- package/src/change-mapping.ts +50 -54
- package/src/index.ts +29 -44
- package/src/native-map.ts +37 -0
- package/src/populate.ts +49 -82
- package/src/substrate.ts +68 -8
- package/src/version.ts +54 -52
- package/src/yjs-resolve.ts +0 -10
- package/src/create.ts +0 -177
- package/src/sync.ts +0 -107
- package/src/yjs-escape.ts +0 -84
package/dist/index.js
CHANGED
|
@@ -2,40 +2,150 @@
|
|
|
2
2
|
import {
|
|
3
3
|
applyChanges,
|
|
4
4
|
change,
|
|
5
|
+
createDoc,
|
|
6
|
+
createRef,
|
|
7
|
+
exportEntirety,
|
|
8
|
+
exportSince,
|
|
9
|
+
merge,
|
|
10
|
+
NATIVE,
|
|
5
11
|
Schema,
|
|
6
12
|
subscribe,
|
|
7
|
-
subscribeNode
|
|
13
|
+
subscribeNode,
|
|
14
|
+
unwrap,
|
|
15
|
+
version
|
|
8
16
|
} from "@kyneta/schema";
|
|
9
17
|
|
|
10
|
-
// src/
|
|
18
|
+
// src/bind-yjs.ts
|
|
11
19
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
registerSubstrate,
|
|
16
|
-
writable
|
|
20
|
+
BACKING_DOC as BACKING_DOC2,
|
|
21
|
+
createSubstrateNamespace,
|
|
22
|
+
STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2
|
|
17
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";
|
|
28
|
+
import * as Y from "yjs";
|
|
29
|
+
function ensureContainers(doc, schema, conditional = false) {
|
|
30
|
+
const rootMap = doc.getMap("root");
|
|
31
|
+
if (schema[KIND] !== "product") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const savedClientID = doc.clientID;
|
|
35
|
+
doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
|
|
36
|
+
try {
|
|
37
|
+
doc.transact(() => {
|
|
38
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(
|
|
39
|
+
([a], [b]) => a.localeCompare(b)
|
|
40
|
+
)) {
|
|
41
|
+
if (conditional && rootMap.has(key)) continue;
|
|
42
|
+
ensureRootField(rootMap, key, fieldSchema);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
} finally {
|
|
46
|
+
doc.clientID = savedClientID;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function ensureRootField(rootMap, key, fieldSchema) {
|
|
50
|
+
switch (fieldSchema[KIND]) {
|
|
51
|
+
case "text":
|
|
52
|
+
rootMap.set(key, new Y.Text());
|
|
53
|
+
return;
|
|
54
|
+
case "product":
|
|
55
|
+
rootMap.set(key, ensureMapContainers(fieldSchema));
|
|
56
|
+
return;
|
|
57
|
+
case "sequence":
|
|
58
|
+
rootMap.set(key, new Y.Array());
|
|
59
|
+
return;
|
|
60
|
+
case "map":
|
|
61
|
+
rootMap.set(key, new Y.Map());
|
|
62
|
+
return;
|
|
63
|
+
case "scalar":
|
|
64
|
+
case "sum": {
|
|
65
|
+
const zero = Zero.structural(fieldSchema);
|
|
66
|
+
if (zero !== void 0) {
|
|
67
|
+
rootMap.set(key, zero);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case "counter":
|
|
72
|
+
case "set":
|
|
73
|
+
case "tree":
|
|
74
|
+
case "movable":
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". Supported kinds: text, product, sequence, map, scalar, sum. Encountered unsupported kind at root field "${key}".`
|
|
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;
|
|
117
|
+
}
|
|
18
118
|
|
|
19
119
|
// src/substrate.ts
|
|
20
|
-
import {
|
|
120
|
+
import {
|
|
121
|
+
BACKING_DOC,
|
|
122
|
+
buildWritableContext,
|
|
123
|
+
executeBatch,
|
|
124
|
+
KIND as KIND3
|
|
125
|
+
} from "@kyneta/schema";
|
|
21
126
|
import * as Y5 from "yjs";
|
|
22
127
|
|
|
23
128
|
// src/change-mapping.ts
|
|
24
|
-
import {
|
|
25
|
-
|
|
129
|
+
import {
|
|
130
|
+
advanceSchema as advanceSchema2,
|
|
131
|
+
expandMapOpsToLeaves,
|
|
132
|
+
KIND as KIND2,
|
|
133
|
+
RawPath
|
|
134
|
+
} from "@kyneta/schema";
|
|
135
|
+
import * as Y3 from "yjs";
|
|
26
136
|
|
|
27
137
|
// src/yjs-resolve.ts
|
|
28
138
|
import { advanceSchema } from "@kyneta/schema";
|
|
29
|
-
import * as
|
|
139
|
+
import * as Y2 from "yjs";
|
|
30
140
|
function stepIntoYjs(current, segment) {
|
|
31
141
|
const resolved = segment.resolve();
|
|
32
|
-
if (current instanceof
|
|
142
|
+
if (current instanceof Y2.Map) {
|
|
33
143
|
return current.get(resolved);
|
|
34
144
|
}
|
|
35
|
-
if (current instanceof
|
|
145
|
+
if (current instanceof Y2.Array) {
|
|
36
146
|
return current.get(resolved);
|
|
37
147
|
}
|
|
38
|
-
if (current instanceof
|
|
148
|
+
if (current instanceof Y2.Text) {
|
|
39
149
|
throw new Error(`yjs-resolve: cannot step into Y.Text`);
|
|
40
150
|
}
|
|
41
151
|
return void 0;
|
|
@@ -43,10 +153,6 @@ function stepIntoYjs(current, segment) {
|
|
|
43
153
|
function resolveYjsType(rootMap, rootSchema, path) {
|
|
44
154
|
let current = rootMap;
|
|
45
155
|
let schema = rootSchema;
|
|
46
|
-
let rootProduct = rootSchema;
|
|
47
|
-
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
48
|
-
rootProduct = rootProduct.schema;
|
|
49
|
-
}
|
|
50
156
|
for (let i = 0; i < path.length; i++) {
|
|
51
157
|
const seg = path.segments[i];
|
|
52
158
|
const nextSchema = advanceSchema(schema, seg);
|
|
@@ -73,11 +179,11 @@ function applyChangeToYjs(rootMap, rootSchema, path, change2) {
|
|
|
73
179
|
return;
|
|
74
180
|
case "increment":
|
|
75
181
|
throw new Error(
|
|
76
|
-
`Yjs substrate does not support
|
|
182
|
+
`Yjs substrate does not support "${change2.type}" changes. Counter requires a CRDT backend that supports counters (e.g. Loro). Attempted IncrementChange with amount=${change2.amount} at path [${pathToString(path)}].`
|
|
77
183
|
);
|
|
78
184
|
case "tree":
|
|
79
185
|
throw new Error(
|
|
80
|
-
`Yjs substrate does not support
|
|
186
|
+
`Yjs substrate does not support "${change2.type}" changes. Tree requires a CRDT backend that supports trees (e.g. Loro). Attempted TreeChange at path [${pathToString(path)}].`
|
|
81
187
|
);
|
|
82
188
|
default:
|
|
83
189
|
throw new Error(
|
|
@@ -87,7 +193,7 @@ function applyChangeToYjs(rootMap, rootSchema, path, change2) {
|
|
|
87
193
|
}
|
|
88
194
|
function applyTextChange(rootMap, rootSchema, path, change2) {
|
|
89
195
|
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
90
|
-
if (!(resolved instanceof
|
|
196
|
+
if (!(resolved instanceof Y3.Text)) {
|
|
91
197
|
throw new Error(
|
|
92
198
|
`applyChangeToYjs: TextChange target at path [${pathToString(path)}] is not a Y.Text`
|
|
93
199
|
);
|
|
@@ -96,7 +202,7 @@ function applyTextChange(rootMap, rootSchema, path, change2) {
|
|
|
96
202
|
}
|
|
97
203
|
function applySequenceChange(rootMap, rootSchema, path, change2) {
|
|
98
204
|
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
99
|
-
if (!(resolved instanceof
|
|
205
|
+
if (!(resolved instanceof Y3.Array)) {
|
|
100
206
|
throw new Error(
|
|
101
207
|
`applyChangeToYjs: SequenceChange target at path [${pathToString(path)}] is not a Y.Array`
|
|
102
208
|
);
|
|
@@ -121,7 +227,7 @@ function applySequenceChange(rootMap, rootSchema, path, change2) {
|
|
|
121
227
|
}
|
|
122
228
|
function applyMapChange(rootMap, rootSchema, path, change2) {
|
|
123
229
|
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
124
|
-
if (!(resolved instanceof
|
|
230
|
+
if (!(resolved instanceof Y3.Map)) {
|
|
125
231
|
throw new Error(
|
|
126
232
|
`applyChangeToYjs: MapChange target at path [${pathToString(path)}] is not a Y.Map`
|
|
127
233
|
);
|
|
@@ -150,11 +256,11 @@ function applyReplaceChange(rootMap, rootSchema, path, change2) {
|
|
|
150
256
|
const parentPath = path.slice(0, -1);
|
|
151
257
|
const parent = resolveYjsType(rootMap, rootSchema, parentPath);
|
|
152
258
|
const resolved = lastSeg.resolve();
|
|
153
|
-
if (parent instanceof
|
|
259
|
+
if (parent instanceof Y3.Map && lastSeg.role === "key") {
|
|
154
260
|
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
155
261
|
const yjsValue = maybeCreateSharedType(change2.value, targetSchema);
|
|
156
262
|
parent.set(resolved, yjsValue);
|
|
157
|
-
} else if (parent instanceof
|
|
263
|
+
} else if (parent instanceof Y3.Array && lastSeg.role === "index") {
|
|
158
264
|
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
159
265
|
const yjsValue = maybeCreateSharedType(change2.value, targetSchema);
|
|
160
266
|
parent.delete(resolved, 1);
|
|
@@ -167,29 +273,25 @@ function applyReplaceChange(rootMap, rootSchema, path, change2) {
|
|
|
167
273
|
}
|
|
168
274
|
function maybeCreateSharedType(value, schema) {
|
|
169
275
|
if (schema === void 0) return value;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
276
|
+
switch (schema[KIND2]) {
|
|
277
|
+
// First-class text → Y.Text
|
|
278
|
+
case "text": {
|
|
279
|
+
const text = new Y3.Text();
|
|
280
|
+
if (typeof value === "string" && value.length > 0) {
|
|
281
|
+
text.insert(0, value);
|
|
282
|
+
}
|
|
283
|
+
return text;
|
|
176
284
|
}
|
|
177
|
-
return text2;
|
|
178
|
-
}
|
|
179
|
-
if (tag === "counter" || tag === "movable" || tag === "tree") {
|
|
180
|
-
throw new Error(`Yjs substrate does not support "${tag}" annotations.`);
|
|
181
|
-
}
|
|
182
|
-
switch (structural._kind) {
|
|
183
285
|
case "product": {
|
|
184
286
|
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
|
|
185
287
|
return value;
|
|
186
288
|
}
|
|
187
|
-
return createStructuredMap(value,
|
|
289
|
+
return createStructuredMap(value, schema);
|
|
188
290
|
}
|
|
189
291
|
case "sequence": {
|
|
190
292
|
if (!Array.isArray(value)) return value;
|
|
191
|
-
const arr = new
|
|
192
|
-
const itemSchema =
|
|
293
|
+
const arr = new Y3.Array();
|
|
294
|
+
const itemSchema = schema.item;
|
|
193
295
|
const items = value.map(
|
|
194
296
|
(item) => maybeCreateSharedType(item, itemSchema)
|
|
195
297
|
);
|
|
@@ -200,21 +302,29 @@ function maybeCreateSharedType(value, schema) {
|
|
|
200
302
|
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
|
|
201
303
|
return value;
|
|
202
304
|
}
|
|
203
|
-
const map = new
|
|
204
|
-
const valueSchema =
|
|
305
|
+
const map = new Y3.Map();
|
|
306
|
+
const valueSchema = schema.item;
|
|
205
307
|
for (const [k, v] of Object.entries(value)) {
|
|
206
308
|
map.set(k, maybeCreateSharedType(v, valueSchema));
|
|
207
309
|
}
|
|
208
310
|
return map;
|
|
209
311
|
}
|
|
312
|
+
// Unsupported first-class CRDT types — should not reach here
|
|
313
|
+
// (rejected at bind time by caps check)
|
|
314
|
+
case "counter":
|
|
315
|
+
case "set":
|
|
316
|
+
case "tree":
|
|
317
|
+
case "movable":
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Yjs substrate does not support [KIND]="${schema[KIND2]}". This should have been caught at bind() time.`
|
|
320
|
+
);
|
|
210
321
|
default:
|
|
211
322
|
return value;
|
|
212
323
|
}
|
|
213
324
|
}
|
|
214
325
|
function createStructuredMap(obj, productSchema) {
|
|
215
|
-
const map = new
|
|
216
|
-
|
|
217
|
-
if (structural._kind !== "product") {
|
|
326
|
+
const map = new Y3.Map();
|
|
327
|
+
if (productSchema[KIND2] !== "product") {
|
|
218
328
|
for (const [key, val] of Object.entries(obj)) {
|
|
219
329
|
map.set(key, val);
|
|
220
330
|
}
|
|
@@ -222,22 +332,21 @@ function createStructuredMap(obj, productSchema) {
|
|
|
222
332
|
}
|
|
223
333
|
for (const [key, val] of Object.entries(obj)) {
|
|
224
334
|
if (val === void 0) continue;
|
|
225
|
-
const fieldSchema =
|
|
335
|
+
const fieldSchema = productSchema.fields[key];
|
|
226
336
|
const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val;
|
|
227
337
|
map.set(key, yjsVal);
|
|
228
338
|
}
|
|
229
339
|
for (const [key, fieldSchema] of Object.entries(
|
|
230
|
-
|
|
340
|
+
productSchema.fields
|
|
231
341
|
)) {
|
|
232
342
|
if (key in obj) continue;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
map.set(key, new Y2.Text());
|
|
343
|
+
if (fieldSchema[KIND2] === "text") {
|
|
344
|
+
map.set(key, new Y3.Text());
|
|
236
345
|
}
|
|
237
346
|
}
|
|
238
347
|
return map;
|
|
239
348
|
}
|
|
240
|
-
function eventsToOps(events) {
|
|
349
|
+
function eventsToOps(events, schema) {
|
|
241
350
|
const ops = [];
|
|
242
351
|
for (const event of events) {
|
|
243
352
|
const kynetaPath = yjsPathToKynetaPath(event.path);
|
|
@@ -246,7 +355,7 @@ function eventsToOps(events) {
|
|
|
246
355
|
ops.push({ path: kynetaPath, change: change2 });
|
|
247
356
|
}
|
|
248
357
|
}
|
|
249
|
-
return expandMapOpsToLeaves(ops);
|
|
358
|
+
return expandMapOpsToLeaves(ops, schema);
|
|
250
359
|
}
|
|
251
360
|
function yjsPathToKynetaPath(yjsPath) {
|
|
252
361
|
let path = RawPath.empty;
|
|
@@ -260,13 +369,13 @@ function yjsPathToKynetaPath(yjsPath) {
|
|
|
260
369
|
return path;
|
|
261
370
|
}
|
|
262
371
|
function eventToChange(event) {
|
|
263
|
-
if (event.target instanceof
|
|
372
|
+
if (event.target instanceof Y3.Text) {
|
|
264
373
|
return textEventToChange(event);
|
|
265
374
|
}
|
|
266
|
-
if (event.target instanceof
|
|
375
|
+
if (event.target instanceof Y3.Array) {
|
|
267
376
|
return arrayEventToChange(event);
|
|
268
377
|
}
|
|
269
|
-
if (event.target instanceof
|
|
378
|
+
if (event.target instanceof Y3.Map) {
|
|
270
379
|
return mapEventToChange(event);
|
|
271
380
|
}
|
|
272
381
|
return null;
|
|
@@ -324,18 +433,11 @@ function mapEventToChange(event) {
|
|
|
324
433
|
};
|
|
325
434
|
}
|
|
326
435
|
function extractEventValue(value) {
|
|
327
|
-
if (value instanceof
|
|
328
|
-
if (value instanceof
|
|
329
|
-
if (value instanceof
|
|
436
|
+
if (value instanceof Y3.Map) return value.toJSON();
|
|
437
|
+
if (value instanceof Y3.Array) return value.toJSON();
|
|
438
|
+
if (value instanceof Y3.Text) return value.toJSON();
|
|
330
439
|
return value;
|
|
331
440
|
}
|
|
332
|
-
function unwrapAnnotations(schema) {
|
|
333
|
-
let s = schema;
|
|
334
|
-
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
335
|
-
s = s.schema;
|
|
336
|
-
}
|
|
337
|
-
return s;
|
|
338
|
-
}
|
|
339
441
|
function resolveSchemaAtPath(rootSchema, path) {
|
|
340
442
|
let schema = rootSchema;
|
|
341
443
|
for (const seg of path.segments) {
|
|
@@ -344,16 +446,19 @@ function resolveSchemaAtPath(rootSchema, path) {
|
|
|
344
446
|
return schema;
|
|
345
447
|
}
|
|
346
448
|
function getItemSchema(schema) {
|
|
347
|
-
|
|
348
|
-
|
|
449
|
+
if (schema[KIND2] === "sequence") return schema.item;
|
|
450
|
+
if (schema[KIND2] === "movable") return schema.item;
|
|
451
|
+
return void 0;
|
|
349
452
|
}
|
|
350
453
|
function getFieldSchema(schema, key) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
454
|
+
if (schema[KIND2] === "product") {
|
|
455
|
+
return schema.fields[key];
|
|
456
|
+
}
|
|
457
|
+
if (schema[KIND2] === "map") {
|
|
458
|
+
return schema.item;
|
|
354
459
|
}
|
|
355
|
-
if (
|
|
356
|
-
return
|
|
460
|
+
if (schema[KIND2] === "set") {
|
|
461
|
+
return schema.item;
|
|
357
462
|
}
|
|
358
463
|
return void 0;
|
|
359
464
|
}
|
|
@@ -361,116 +466,6 @@ function pathToString(path) {
|
|
|
361
466
|
return path.segments.map((seg) => String(seg.resolve())).join(".");
|
|
362
467
|
}
|
|
363
468
|
|
|
364
|
-
// src/populate.ts
|
|
365
|
-
import { STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
|
|
366
|
-
import * as Y3 from "yjs";
|
|
367
|
-
function ensureContainers(doc, schema, conditional = false) {
|
|
368
|
-
const rootMap = doc.getMap("root");
|
|
369
|
-
let rootProduct = schema;
|
|
370
|
-
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
371
|
-
rootProduct = rootProduct.schema;
|
|
372
|
-
}
|
|
373
|
-
if (rootProduct._kind !== "product") {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const savedClientID = doc.clientID;
|
|
377
|
-
doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
|
|
378
|
-
try {
|
|
379
|
-
doc.transact(() => {
|
|
380
|
-
for (const [key, fieldSchema] of Object.entries(rootProduct.fields).sort(
|
|
381
|
-
([a], [b]) => a.localeCompare(b)
|
|
382
|
-
)) {
|
|
383
|
-
if (conditional && rootMap.has(key)) continue;
|
|
384
|
-
ensureRootField(rootMap, key, fieldSchema);
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
} finally {
|
|
388
|
-
doc.clientID = savedClientID;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
function ensureRootField(rootMap, key, fieldSchema) {
|
|
392
|
-
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
|
|
393
|
-
switch (tag) {
|
|
394
|
-
case "text":
|
|
395
|
-
rootMap.set(key, new Y3.Text());
|
|
396
|
-
return;
|
|
397
|
-
case "counter":
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Yjs substrate does not support counter annotations. Use Schema.number() with ReplaceChange instead. Encountered counter annotation at root field "${key}".`
|
|
400
|
-
);
|
|
401
|
-
case "movable":
|
|
402
|
-
throw new Error(
|
|
403
|
-
`Yjs substrate does not support movable list annotations. Yjs has no native movable list type. Encountered movable annotation at root field "${key}".`
|
|
404
|
-
);
|
|
405
|
-
case "tree":
|
|
406
|
-
throw new Error(
|
|
407
|
-
`Yjs substrate does not support tree annotations. Yjs has no native tree type. Encountered tree annotation at root field "${key}".`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
const structural = unwrapAnnotations2(fieldSchema);
|
|
411
|
-
switch (structural._kind) {
|
|
412
|
-
case "product":
|
|
413
|
-
rootMap.set(key, ensureMapContainers(structural));
|
|
414
|
-
return;
|
|
415
|
-
case "sequence":
|
|
416
|
-
rootMap.set(key, new Y3.Array());
|
|
417
|
-
return;
|
|
418
|
-
case "map":
|
|
419
|
-
rootMap.set(key, new Y3.Map());
|
|
420
|
-
return;
|
|
421
|
-
case "scalar":
|
|
422
|
-
case "sum": {
|
|
423
|
-
const zero = Zero.structural(fieldSchema);
|
|
424
|
-
if (zero !== void 0) {
|
|
425
|
-
rootMap.set(key, zero);
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
function ensureMapContainers(schema) {
|
|
432
|
-
const map = new Y3.Map();
|
|
433
|
-
const structural = unwrapAnnotations2(schema);
|
|
434
|
-
if (structural._kind !== "product") return map;
|
|
435
|
-
for (const [key, fieldSchema] of Object.entries(
|
|
436
|
-
structural.fields
|
|
437
|
-
).sort(([a], [b]) => a.localeCompare(b))) {
|
|
438
|
-
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
|
|
439
|
-
if (tag === "text") {
|
|
440
|
-
map.set(key, new Y3.Text());
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
const fs = unwrapAnnotations2(fieldSchema);
|
|
444
|
-
switch (fs._kind) {
|
|
445
|
-
case "product":
|
|
446
|
-
map.set(key, ensureMapContainers(fieldSchema));
|
|
447
|
-
break;
|
|
448
|
-
case "sequence":
|
|
449
|
-
map.set(key, new Y3.Array());
|
|
450
|
-
break;
|
|
451
|
-
case "map":
|
|
452
|
-
map.set(key, new Y3.Map());
|
|
453
|
-
break;
|
|
454
|
-
case "scalar":
|
|
455
|
-
case "sum": {
|
|
456
|
-
const zero = Zero.structural(fieldSchema);
|
|
457
|
-
if (zero !== void 0) {
|
|
458
|
-
map.set(key, zero);
|
|
459
|
-
}
|
|
460
|
-
break;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return map;
|
|
465
|
-
}
|
|
466
|
-
function unwrapAnnotations2(schema) {
|
|
467
|
-
let s = schema;
|
|
468
|
-
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
469
|
-
s = s.schema;
|
|
470
|
-
}
|
|
471
|
-
return s;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
469
|
// src/reader.ts
|
|
475
470
|
import * as Y4 from "yjs";
|
|
476
471
|
function extractValue(resolved) {
|
|
@@ -529,21 +524,28 @@ function yjsReader(doc, schema) {
|
|
|
529
524
|
}
|
|
530
525
|
|
|
531
526
|
// src/version.ts
|
|
527
|
+
import {
|
|
528
|
+
base64ToUint8Array,
|
|
529
|
+
uint8ArrayToBase64,
|
|
530
|
+
versionVectorCompare,
|
|
531
|
+
versionVectorMeet
|
|
532
|
+
} from "@kyneta/schema";
|
|
532
533
|
import { decodeStateVector } from "yjs";
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
534
|
+
function encodeStateVector(map) {
|
|
535
|
+
const bytes = [];
|
|
536
|
+
function writeVarUint(value) {
|
|
537
|
+
while (value > 127) {
|
|
538
|
+
bytes.push(value & 127 | 128);
|
|
539
|
+
value >>>= 7;
|
|
540
|
+
}
|
|
541
|
+
bytes.push(value & 127);
|
|
537
542
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const bytes = new Uint8Array(binary.length);
|
|
543
|
-
for (let i = 0; i < binary.length; i++) {
|
|
544
|
-
bytes[i] = binary.charCodeAt(i);
|
|
543
|
+
writeVarUint(map.size);
|
|
544
|
+
for (const [clientId, clock] of map) {
|
|
545
|
+
writeVarUint(clientId);
|
|
546
|
+
writeVarUint(clock);
|
|
545
547
|
}
|
|
546
|
-
return bytes;
|
|
548
|
+
return new Uint8Array(bytes);
|
|
547
549
|
}
|
|
548
550
|
var YjsVersion = class _YjsVersion {
|
|
549
551
|
sv;
|
|
@@ -562,45 +564,37 @@ var YjsVersion = class _YjsVersion {
|
|
|
562
564
|
/**
|
|
563
565
|
* Compare with another version using version-vector partial order.
|
|
564
566
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
+
* Delegates to the shared `versionVectorCompare` utility after decoding
|
|
568
|
+
* both state vectors via `Y.decodeStateVector()`.
|
|
567
569
|
*
|
|
568
|
-
*
|
|
569
|
-
* - For each client, compare clocks (missing client = clock 0).
|
|
570
|
-
* - If all clocks in `this` ≤ `other` and at least one strictly less → `"behind"`
|
|
571
|
-
* - If all clocks in `this` ≥ `other` and at least one strictly greater → `"ahead"`
|
|
572
|
-
* - If all clocks equal → `"equal"`
|
|
573
|
-
* - Otherwise → `"concurrent"`
|
|
574
|
-
*
|
|
575
|
-
* Throws if `other` is not a `YjsVersion`.
|
|
570
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
576
571
|
*/
|
|
577
572
|
compare(other) {
|
|
578
573
|
if (!(other instanceof _YjsVersion)) {
|
|
579
574
|
throw new Error("YjsVersion can only be compared with another YjsVersion");
|
|
580
575
|
}
|
|
576
|
+
return versionVectorCompare(
|
|
577
|
+
decodeStateVector(this.sv),
|
|
578
|
+
decodeStateVector(other.sv)
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Greatest lower bound (lattice meet) of two Yjs versions.
|
|
583
|
+
*
|
|
584
|
+
* Decodes both state vectors, computes the component-wise minimum
|
|
585
|
+
* via the shared `versionVectorMeet` utility, and encodes the result
|
|
586
|
+
* back to a Yjs state vector.
|
|
587
|
+
*
|
|
588
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
589
|
+
*/
|
|
590
|
+
meet(other) {
|
|
591
|
+
if (!(other instanceof _YjsVersion)) {
|
|
592
|
+
throw new Error("YjsVersion can only be meet'd with another YjsVersion");
|
|
593
|
+
}
|
|
581
594
|
const thisMap = decodeStateVector(this.sv);
|
|
582
595
|
const otherMap = decodeStateVector(other.sv);
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
for (const id of otherMap.keys()) allClients.add(id);
|
|
586
|
-
let hasLess = false;
|
|
587
|
-
let hasGreater = false;
|
|
588
|
-
for (const clientId of allClients) {
|
|
589
|
-
const thisClock = thisMap.get(clientId) ?? 0;
|
|
590
|
-
const otherClock = otherMap.get(clientId) ?? 0;
|
|
591
|
-
if (thisClock < otherClock) {
|
|
592
|
-
hasLess = true;
|
|
593
|
-
}
|
|
594
|
-
if (thisClock > otherClock) {
|
|
595
|
-
hasGreater = true;
|
|
596
|
-
}
|
|
597
|
-
if (hasLess && hasGreater) {
|
|
598
|
-
return "concurrent";
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (hasLess && !hasGreater) return "behind";
|
|
602
|
-
if (hasGreater && !hasLess) return "ahead";
|
|
603
|
-
return "equal";
|
|
596
|
+
const result = versionVectorMeet(thisMap, otherMap);
|
|
597
|
+
return new _YjsVersion(encodeStateVector(result));
|
|
604
598
|
}
|
|
605
599
|
/**
|
|
606
600
|
* Parse a serialized YjsVersion string back into a YjsVersion.
|
|
@@ -633,7 +627,7 @@ function createYjsSubstrate(doc, schema) {
|
|
|
633
627
|
pendingChanges.push({ path, change: change2 });
|
|
634
628
|
}
|
|
635
629
|
},
|
|
636
|
-
onFlush(
|
|
630
|
+
onFlush(_origin) {
|
|
637
631
|
if (!inOurTransaction && pendingChanges.length > 0) {
|
|
638
632
|
inOurTransaction = true;
|
|
639
633
|
try {
|
|
@@ -651,12 +645,26 @@ function createYjsSubstrate(doc, schema) {
|
|
|
651
645
|
context() {
|
|
652
646
|
if (!cachedCtx) {
|
|
653
647
|
cachedCtx = buildWritableContext(substrate);
|
|
648
|
+
cachedCtx.nativeResolver = (nodeSchema, path) => {
|
|
649
|
+
if (path.segments.length === 0) return doc;
|
|
650
|
+
if (nodeSchema[KIND3] === "scalar" || nodeSchema[KIND3] === "sum")
|
|
651
|
+
return void 0;
|
|
652
|
+
return resolveYjsType(rootMap, schema, path);
|
|
653
|
+
};
|
|
654
654
|
}
|
|
655
655
|
return cachedCtx;
|
|
656
656
|
},
|
|
657
657
|
version() {
|
|
658
658
|
return new YjsVersion(Y5.encodeStateVector(doc));
|
|
659
659
|
},
|
|
660
|
+
baseVersion() {
|
|
661
|
+
return new YjsVersion(new Uint8Array([0]));
|
|
662
|
+
},
|
|
663
|
+
advance(_to) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"advance() on a live Yjs substrate is not yet supported. Use advance() on a YjsReplica instead."
|
|
666
|
+
);
|
|
667
|
+
},
|
|
660
668
|
exportEntirety() {
|
|
661
669
|
return {
|
|
662
670
|
kind: "entirety",
|
|
@@ -690,7 +698,7 @@ function createYjsSubstrate(doc, schema) {
|
|
|
690
698
|
if (transaction.origin === KYNETA_ORIGIN) {
|
|
691
699
|
return;
|
|
692
700
|
}
|
|
693
|
-
const ops = eventsToOps(events);
|
|
701
|
+
const ops = eventsToOps(events, schema);
|
|
694
702
|
if (ops.length === 0) {
|
|
695
703
|
return;
|
|
696
704
|
}
|
|
@@ -706,21 +714,44 @@ function createYjsSubstrate(doc, schema) {
|
|
|
706
714
|
return substrate;
|
|
707
715
|
}
|
|
708
716
|
function createYjsReplica(doc) {
|
|
717
|
+
let currentDoc = doc;
|
|
718
|
+
let currentBase = new YjsVersion(Y5.encodeStateVector(new Y5.Doc()));
|
|
709
719
|
return {
|
|
710
|
-
[BACKING_DOC]
|
|
720
|
+
get [BACKING_DOC]() {
|
|
721
|
+
return currentDoc;
|
|
722
|
+
},
|
|
711
723
|
version() {
|
|
712
|
-
return new YjsVersion(Y5.encodeStateVector(
|
|
724
|
+
return new YjsVersion(Y5.encodeStateVector(currentDoc));
|
|
725
|
+
},
|
|
726
|
+
baseVersion() {
|
|
727
|
+
return currentBase;
|
|
728
|
+
},
|
|
729
|
+
advance(to) {
|
|
730
|
+
const baseCmp = currentBase.compare(to);
|
|
731
|
+
if (baseCmp === "ahead") {
|
|
732
|
+
throw new Error("advance(): target is behind base version");
|
|
733
|
+
}
|
|
734
|
+
const currentCmp = to.compare(this.version());
|
|
735
|
+
if (currentCmp === "ahead") {
|
|
736
|
+
throw new Error("advance(): target is ahead of current version");
|
|
737
|
+
}
|
|
738
|
+
if (currentCmp !== "equal") return;
|
|
739
|
+
const update = Y5.encodeStateAsUpdate(currentDoc);
|
|
740
|
+
const newDoc = new Y5.Doc();
|
|
741
|
+
Y5.applyUpdate(newDoc, update);
|
|
742
|
+
currentDoc = newDoc;
|
|
743
|
+
currentBase = new YjsVersion(Y5.encodeStateVector(currentDoc));
|
|
713
744
|
},
|
|
714
745
|
exportEntirety() {
|
|
715
746
|
return {
|
|
716
747
|
kind: "entirety",
|
|
717
748
|
encoding: "binary",
|
|
718
|
-
data: Y5.encodeStateAsUpdate(
|
|
749
|
+
data: Y5.encodeStateAsUpdate(currentDoc)
|
|
719
750
|
};
|
|
720
751
|
},
|
|
721
752
|
exportSince(since) {
|
|
722
753
|
try {
|
|
723
|
-
const bytes = Y5.encodeStateAsUpdate(
|
|
754
|
+
const bytes = Y5.encodeStateAsUpdate(currentDoc, since.sv);
|
|
724
755
|
return { kind: "since", encoding: "binary", data: bytes };
|
|
725
756
|
} catch {
|
|
726
757
|
return null;
|
|
@@ -732,7 +763,7 @@ function createYjsReplica(doc) {
|
|
|
732
763
|
"YjsReplica.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
|
|
733
764
|
);
|
|
734
765
|
}
|
|
735
|
-
Y5.applyUpdate(
|
|
766
|
+
Y5.applyUpdate(currentDoc, payload.data);
|
|
736
767
|
}
|
|
737
768
|
};
|
|
738
769
|
}
|
|
@@ -780,55 +811,7 @@ var yjsSubstrateFactory = {
|
|
|
780
811
|
}
|
|
781
812
|
};
|
|
782
813
|
|
|
783
|
-
// src/create.ts
|
|
784
|
-
var substrates = /* @__PURE__ */ new WeakMap();
|
|
785
|
-
function getSubstrate(doc) {
|
|
786
|
-
const s = substrates.get(doc);
|
|
787
|
-
if (!s) {
|
|
788
|
-
throw new Error(
|
|
789
|
-
"version/exportEntirety/merge called on an object without a YjsSubstrate. Use a doc created by createYjsDoc() or createYjsDocFromEntirety()."
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
return s;
|
|
793
|
-
}
|
|
794
|
-
function registerDoc(schema, substrate) {
|
|
795
|
-
const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(changefeed).done();
|
|
796
|
-
substrates.set(doc, substrate);
|
|
797
|
-
registerSubstrate(doc, substrate);
|
|
798
|
-
return doc;
|
|
799
|
-
}
|
|
800
|
-
function isYDoc(value) {
|
|
801
|
-
return value !== null && value !== void 0 && typeof value === "object" && "getMap" in value && "getText" in value && "getArray" in value && "transact" in value && typeof value.transact === "function" && // Y.Doc has clientID; distinguish from other objects
|
|
802
|
-
"clientID" in value && typeof value.clientID === "number";
|
|
803
|
-
}
|
|
804
|
-
var createYjsDoc = (schema, doc) => {
|
|
805
|
-
if (doc !== void 0 && isYDoc(doc)) {
|
|
806
|
-
return registerDoc(schema, createYjsSubstrate(doc, schema));
|
|
807
|
-
}
|
|
808
|
-
return registerDoc(schema, yjsSubstrateFactory.create(schema));
|
|
809
|
-
};
|
|
810
|
-
var createYjsDocFromEntirety = (schema, payload) => registerDoc(schema, yjsSubstrateFactory.fromEntirety(payload, schema));
|
|
811
|
-
|
|
812
|
-
// src/sync.ts
|
|
813
|
-
function version(doc) {
|
|
814
|
-
return getSubstrate(doc).version();
|
|
815
|
-
}
|
|
816
|
-
function exportEntirety(doc) {
|
|
817
|
-
return getSubstrate(doc).exportEntirety();
|
|
818
|
-
}
|
|
819
|
-
function exportSince(doc, since) {
|
|
820
|
-
return getSubstrate(doc).exportSince(since);
|
|
821
|
-
}
|
|
822
|
-
function merge(doc, payload, origin) {
|
|
823
|
-
getSubstrate(doc).merge(payload, origin);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// src/index.ts
|
|
827
|
-
import { Schema as Schema2 } from "@kyneta/schema";
|
|
828
|
-
|
|
829
814
|
// src/bind-yjs.ts
|
|
830
|
-
import { BACKING_DOC as BACKING_DOC2, bind, STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2 } from "@kyneta/schema";
|
|
831
|
-
import * as Y6 from "yjs";
|
|
832
815
|
function hashPeerId(peerId) {
|
|
833
816
|
let hash = 2166136261;
|
|
834
817
|
for (let i = 0; i < peerId.length; i++) {
|
|
@@ -867,47 +850,28 @@ function createYjsFactory(peerId) {
|
|
|
867
850
|
}
|
|
868
851
|
};
|
|
869
852
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
substrate = unwrap(ref);
|
|
884
|
-
} catch {
|
|
885
|
-
throw new Error(
|
|
886
|
-
"yjs() requires a ref backed by a Yjs substrate. Use a doc created by exchange.get() with a bindYjs() schema, or by createYjsDoc()."
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
const doc = substrate[BACKING_DOC3];
|
|
890
|
-
if (!doc || typeof doc !== "object" || typeof doc.getMap !== "function" || typeof doc.clientID !== "number") {
|
|
891
|
-
throw new Error(
|
|
892
|
-
"yjs() requires a ref backed by a Yjs substrate. The ref has a substrate but it is not a Yjs substrate. Use a doc created with a bindYjs() schema or createYjsDoc()."
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
return doc;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// src/index.ts
|
|
899
|
-
function text() {
|
|
900
|
-
return Schema2.annotated("text");
|
|
901
|
-
}
|
|
853
|
+
var yjs = createSubstrateNamespace({
|
|
854
|
+
strategies: {
|
|
855
|
+
collaborative: {
|
|
856
|
+
factory: (ctx) => createYjsFactory(ctx.peerId),
|
|
857
|
+
replicaFactory: yjsReplicaFactory
|
|
858
|
+
},
|
|
859
|
+
ephemeral: {
|
|
860
|
+
factory: (ctx) => createYjsFactory(ctx.peerId),
|
|
861
|
+
replicaFactory: yjsReplicaFactory
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
defaultStrategy: "collaborative"
|
|
865
|
+
});
|
|
902
866
|
export {
|
|
867
|
+
NATIVE,
|
|
903
868
|
Schema,
|
|
904
869
|
YjsVersion,
|
|
905
870
|
applyChangeToYjs,
|
|
906
871
|
applyChanges,
|
|
907
|
-
bindYjs,
|
|
908
872
|
change,
|
|
909
|
-
|
|
910
|
-
|
|
873
|
+
createDoc,
|
|
874
|
+
createRef,
|
|
911
875
|
createYjsSubstrate,
|
|
912
876
|
ensureContainers,
|
|
913
877
|
eventsToOps,
|
|
@@ -918,10 +882,11 @@ export {
|
|
|
918
882
|
stepIntoYjs,
|
|
919
883
|
subscribe,
|
|
920
884
|
subscribeNode,
|
|
921
|
-
|
|
885
|
+
unwrap,
|
|
922
886
|
version,
|
|
923
887
|
yjs,
|
|
924
888
|
yjsReader,
|
|
889
|
+
yjsReplicaFactory,
|
|
925
890
|
yjsSubstrateFactory
|
|
926
891
|
};
|
|
927
892
|
//# sourceMappingURL=index.js.map
|