@kyneta/yjs-schema 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +109 -147
- package/dist/index.js +321 -210
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +333 -0
- package/src/__tests__/bind-yjs.test.ts +53 -55
- package/src/__tests__/create.test.ts +71 -62
- package/src/__tests__/{store-reader.test.ts → reader.test.ts} +64 -90
- package/src/__tests__/record-text-spike.test.ts +38 -31
- package/src/__tests__/structural-merge.test.ts +362 -0
- package/src/__tests__/substrate.test.ts +65 -84
- package/src/__tests__/version.test.ts +82 -16
- package/src/bind-yjs.ts +115 -64
- package/src/change-mapping.ts +60 -84
- package/src/create.ts +33 -28
- package/src/index.ts +32 -51
- package/src/populate.ts +87 -92
- package/src/{store-reader.ts → reader.ts} +7 -12
- package/src/substrate.ts +186 -42
- package/src/sync.ts +26 -26
- package/src/version.ts +57 -4
- package/src/yjs-resolve.ts +5 -21
- package/src/yjs-escape.ts +0 -100
package/dist/index.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
applyChanges,
|
|
4
|
+
change,
|
|
5
|
+
Schema,
|
|
6
|
+
subscribe,
|
|
7
|
+
subscribeNode
|
|
8
|
+
} from "@kyneta/schema";
|
|
9
|
+
|
|
1
10
|
// src/create.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
11
|
+
import {
|
|
12
|
+
interpret,
|
|
13
|
+
observation,
|
|
14
|
+
readable,
|
|
15
|
+
registerSubstrate,
|
|
16
|
+
writable
|
|
17
|
+
} from "@kyneta/schema";
|
|
4
18
|
|
|
5
19
|
// src/substrate.ts
|
|
6
|
-
import { buildWritableContext, executeBatch } from "@kyneta/schema";
|
|
20
|
+
import { BACKING_DOC, buildWritableContext, executeBatch } from "@kyneta/schema";
|
|
7
21
|
import * as Y5 from "yjs";
|
|
8
22
|
|
|
9
23
|
// src/change-mapping.ts
|
|
10
|
-
import { advanceSchema as advanceSchema2, expandMapOpsToLeaves } from "@kyneta/schema";
|
|
11
|
-
import { RawPath } from "@kyneta/schema";
|
|
24
|
+
import { advanceSchema as advanceSchema2, expandMapOpsToLeaves, KIND as KIND2, RawPath } from "@kyneta/schema";
|
|
12
25
|
import * as Y2 from "yjs";
|
|
13
26
|
|
|
14
27
|
// src/yjs-resolve.ts
|
|
@@ -23,19 +36,13 @@ function stepIntoYjs(current, segment) {
|
|
|
23
36
|
return current.get(resolved);
|
|
24
37
|
}
|
|
25
38
|
if (current instanceof Y.Text) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`yjs-resolve: cannot step into Y.Text`
|
|
28
|
-
);
|
|
39
|
+
throw new Error(`yjs-resolve: cannot step into Y.Text`);
|
|
29
40
|
}
|
|
30
41
|
return void 0;
|
|
31
42
|
}
|
|
32
43
|
function resolveYjsType(rootMap, rootSchema, path) {
|
|
33
44
|
let current = rootMap;
|
|
34
45
|
let schema = rootSchema;
|
|
35
|
-
let rootProduct = rootSchema;
|
|
36
|
-
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
37
|
-
rootProduct = rootProduct.schema;
|
|
38
|
-
}
|
|
39
46
|
for (let i = 0; i < path.length; i++) {
|
|
40
47
|
const seg = path.segments[i];
|
|
41
48
|
const nextSchema = advanceSchema(schema, seg);
|
|
@@ -62,11 +69,11 @@ function applyChangeToYjs(rootMap, rootSchema, path, change2) {
|
|
|
62
69
|
return;
|
|
63
70
|
case "increment":
|
|
64
71
|
throw new Error(
|
|
65
|
-
`Yjs substrate does not support
|
|
72
|
+
`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)}].`
|
|
66
73
|
);
|
|
67
74
|
case "tree":
|
|
68
75
|
throw new Error(
|
|
69
|
-
`Yjs substrate does not support
|
|
76
|
+
`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)}].`
|
|
70
77
|
);
|
|
71
78
|
default:
|
|
72
79
|
throw new Error(
|
|
@@ -156,34 +163,25 @@ function applyReplaceChange(rootMap, rootSchema, path, change2) {
|
|
|
156
163
|
}
|
|
157
164
|
function maybeCreateSharedType(value, schema) {
|
|
158
165
|
if (schema === void 0) return value;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
switch (schema[KIND2]) {
|
|
167
|
+
// First-class text → Y.Text
|
|
168
|
+
case "text": {
|
|
169
|
+
const text = new Y2.Text();
|
|
170
|
+
if (typeof value === "string" && value.length > 0) {
|
|
171
|
+
text.insert(0, value);
|
|
172
|
+
}
|
|
173
|
+
return text;
|
|
165
174
|
}
|
|
166
|
-
return text2;
|
|
167
|
-
}
|
|
168
|
-
if (tag === "counter" || tag === "movable" || tag === "tree") {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Yjs substrate does not support "${tag}" annotations.`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
switch (structural._kind) {
|
|
174
175
|
case "product": {
|
|
175
176
|
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
|
|
176
177
|
return value;
|
|
177
178
|
}
|
|
178
|
-
return createStructuredMap(
|
|
179
|
-
value,
|
|
180
|
-
structural
|
|
181
|
-
);
|
|
179
|
+
return createStructuredMap(value, schema);
|
|
182
180
|
}
|
|
183
181
|
case "sequence": {
|
|
184
182
|
if (!Array.isArray(value)) return value;
|
|
185
183
|
const arr = new Y2.Array();
|
|
186
|
-
const itemSchema =
|
|
184
|
+
const itemSchema = schema.item;
|
|
187
185
|
const items = value.map(
|
|
188
186
|
(item) => maybeCreateSharedType(item, itemSchema)
|
|
189
187
|
);
|
|
@@ -195,22 +193,28 @@ function maybeCreateSharedType(value, schema) {
|
|
|
195
193
|
return value;
|
|
196
194
|
}
|
|
197
195
|
const map = new Y2.Map();
|
|
198
|
-
const valueSchema =
|
|
199
|
-
for (const [k, v] of Object.entries(
|
|
200
|
-
value
|
|
201
|
-
)) {
|
|
196
|
+
const valueSchema = schema.item;
|
|
197
|
+
for (const [k, v] of Object.entries(value)) {
|
|
202
198
|
map.set(k, maybeCreateSharedType(v, valueSchema));
|
|
203
199
|
}
|
|
204
200
|
return map;
|
|
205
201
|
}
|
|
202
|
+
// Unsupported first-class CRDT types — should not reach here
|
|
203
|
+
// (rejected at bind time by caps check)
|
|
204
|
+
case "counter":
|
|
205
|
+
case "set":
|
|
206
|
+
case "tree":
|
|
207
|
+
case "movable":
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Yjs substrate does not support [KIND]="${schema[KIND2]}". This should have been caught at bind() time.`
|
|
210
|
+
);
|
|
206
211
|
default:
|
|
207
212
|
return value;
|
|
208
213
|
}
|
|
209
214
|
}
|
|
210
215
|
function createStructuredMap(obj, productSchema) {
|
|
211
216
|
const map = new Y2.Map();
|
|
212
|
-
|
|
213
|
-
if (structural._kind !== "product") {
|
|
217
|
+
if (productSchema[KIND2] !== "product") {
|
|
214
218
|
for (const [key, val] of Object.entries(obj)) {
|
|
215
219
|
map.set(key, val);
|
|
216
220
|
}
|
|
@@ -218,22 +222,21 @@ function createStructuredMap(obj, productSchema) {
|
|
|
218
222
|
}
|
|
219
223
|
for (const [key, val] of Object.entries(obj)) {
|
|
220
224
|
if (val === void 0) continue;
|
|
221
|
-
const fieldSchema =
|
|
225
|
+
const fieldSchema = productSchema.fields[key];
|
|
222
226
|
const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val;
|
|
223
227
|
map.set(key, yjsVal);
|
|
224
228
|
}
|
|
225
229
|
for (const [key, fieldSchema] of Object.entries(
|
|
226
|
-
|
|
230
|
+
productSchema.fields
|
|
227
231
|
)) {
|
|
228
232
|
if (key in obj) continue;
|
|
229
|
-
|
|
230
|
-
if (tag === "text") {
|
|
233
|
+
if (fieldSchema[KIND2] === "text") {
|
|
231
234
|
map.set(key, new Y2.Text());
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
return map;
|
|
235
238
|
}
|
|
236
|
-
function eventsToOps(events) {
|
|
239
|
+
function eventsToOps(events, schema) {
|
|
237
240
|
const ops = [];
|
|
238
241
|
for (const event of events) {
|
|
239
242
|
const kynetaPath = yjsPathToKynetaPath(event.path);
|
|
@@ -242,7 +245,7 @@ function eventsToOps(events) {
|
|
|
242
245
|
ops.push({ path: kynetaPath, change: change2 });
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
|
-
return expandMapOpsToLeaves(ops);
|
|
248
|
+
return expandMapOpsToLeaves(ops, schema);
|
|
246
249
|
}
|
|
247
250
|
function yjsPathToKynetaPath(yjsPath) {
|
|
248
251
|
let path = RawPath.empty;
|
|
@@ -302,18 +305,16 @@ function mapEventToChange(event) {
|
|
|
302
305
|
let hasSet = false;
|
|
303
306
|
let hasDelete = false;
|
|
304
307
|
const target = event.target;
|
|
305
|
-
event.changes.keys.forEach(
|
|
306
|
-
(change2
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
hasDelete = true;
|
|
314
|
-
}
|
|
308
|
+
event.changes.keys.forEach((change2, key) => {
|
|
309
|
+
if (change2.action === "add" || change2.action === "update") {
|
|
310
|
+
const value = target.get(key);
|
|
311
|
+
set[key] = extractEventValue(value);
|
|
312
|
+
hasSet = true;
|
|
313
|
+
} else if (change2.action === "delete") {
|
|
314
|
+
deleteKeys.push(key);
|
|
315
|
+
hasDelete = true;
|
|
315
316
|
}
|
|
316
|
-
);
|
|
317
|
+
});
|
|
317
318
|
if (!hasSet && !hasDelete) return null;
|
|
318
319
|
return {
|
|
319
320
|
type: "map",
|
|
@@ -327,13 +328,6 @@ function extractEventValue(value) {
|
|
|
327
328
|
if (value instanceof Y2.Text) return value.toJSON();
|
|
328
329
|
return value;
|
|
329
330
|
}
|
|
330
|
-
function unwrapAnnotations(schema) {
|
|
331
|
-
let s = schema;
|
|
332
|
-
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
333
|
-
s = s.schema;
|
|
334
|
-
}
|
|
335
|
-
return s;
|
|
336
|
-
}
|
|
337
331
|
function resolveSchemaAtPath(rootSchema, path) {
|
|
338
332
|
let schema = rootSchema;
|
|
339
333
|
for (const seg of path.segments) {
|
|
@@ -342,16 +336,19 @@ function resolveSchemaAtPath(rootSchema, path) {
|
|
|
342
336
|
return schema;
|
|
343
337
|
}
|
|
344
338
|
function getItemSchema(schema) {
|
|
345
|
-
|
|
346
|
-
|
|
339
|
+
if (schema[KIND2] === "sequence") return schema.item;
|
|
340
|
+
if (schema[KIND2] === "movable") return schema.item;
|
|
341
|
+
return void 0;
|
|
347
342
|
}
|
|
348
343
|
function getFieldSchema(schema, key) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
if (schema[KIND2] === "product") {
|
|
345
|
+
return schema.fields[key];
|
|
346
|
+
}
|
|
347
|
+
if (schema[KIND2] === "map") {
|
|
348
|
+
return schema.item;
|
|
352
349
|
}
|
|
353
|
-
if (
|
|
354
|
-
return
|
|
350
|
+
if (schema[KIND2] === "set") {
|
|
351
|
+
return schema.item;
|
|
355
352
|
}
|
|
356
353
|
return void 0;
|
|
357
354
|
}
|
|
@@ -360,46 +357,35 @@ function pathToString(path) {
|
|
|
360
357
|
}
|
|
361
358
|
|
|
362
359
|
// src/populate.ts
|
|
363
|
-
import { Zero } from "@kyneta/schema";
|
|
360
|
+
import { KIND as KIND3, STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
|
|
364
361
|
import * as Y3 from "yjs";
|
|
365
|
-
function ensureContainers(doc, schema) {
|
|
362
|
+
function ensureContainers(doc, schema, conditional = false) {
|
|
366
363
|
const rootMap = doc.getMap("root");
|
|
367
|
-
|
|
368
|
-
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
369
|
-
rootProduct = rootProduct.schema;
|
|
370
|
-
}
|
|
371
|
-
if (rootProduct._kind !== "product") {
|
|
364
|
+
if (schema[KIND3] !== "product") {
|
|
372
365
|
return;
|
|
373
366
|
}
|
|
374
|
-
doc.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
367
|
+
const savedClientID = doc.clientID;
|
|
368
|
+
doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
|
|
369
|
+
try {
|
|
370
|
+
doc.transact(() => {
|
|
371
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(
|
|
372
|
+
([a], [b]) => a.localeCompare(b)
|
|
373
|
+
)) {
|
|
374
|
+
if (conditional && rootMap.has(key)) continue;
|
|
375
|
+
ensureRootField(rootMap, key, fieldSchema);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
} finally {
|
|
379
|
+
doc.clientID = savedClientID;
|
|
380
|
+
}
|
|
379
381
|
}
|
|
380
382
|
function ensureRootField(rootMap, key, fieldSchema) {
|
|
381
|
-
|
|
382
|
-
switch (tag) {
|
|
383
|
+
switch (fieldSchema[KIND3]) {
|
|
383
384
|
case "text":
|
|
384
385
|
rootMap.set(key, new Y3.Text());
|
|
385
386
|
return;
|
|
386
|
-
case "counter":
|
|
387
|
-
throw new Error(
|
|
388
|
-
`Yjs substrate does not support counter annotations. Use Schema.number() with ReplaceChange instead. Encountered counter annotation at root field "${key}".`
|
|
389
|
-
);
|
|
390
|
-
case "movable":
|
|
391
|
-
throw new Error(
|
|
392
|
-
`Yjs substrate does not support movable list annotations. Yjs has no native movable list type. Encountered movable annotation at root field "${key}".`
|
|
393
|
-
);
|
|
394
|
-
case "tree":
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Yjs substrate does not support tree annotations. Yjs has no native tree type. Encountered tree annotation at root field "${key}".`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const structural = unwrapAnnotations2(fieldSchema);
|
|
400
|
-
switch (structural._kind) {
|
|
401
387
|
case "product":
|
|
402
|
-
rootMap.set(key, ensureMapContainers(
|
|
388
|
+
rootMap.set(key, ensureMapContainers(fieldSchema));
|
|
403
389
|
return;
|
|
404
390
|
case "sequence":
|
|
405
391
|
rootMap.set(key, new Y3.Array());
|
|
@@ -415,22 +401,25 @@ function ensureRootField(rootMap, key, fieldSchema) {
|
|
|
415
401
|
}
|
|
416
402
|
return;
|
|
417
403
|
}
|
|
404
|
+
case "counter":
|
|
405
|
+
case "set":
|
|
406
|
+
case "tree":
|
|
407
|
+
case "movable":
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Yjs substrate does not support [KIND]="${fieldSchema[KIND3]}". Supported kinds: text, product, sequence, map, scalar, sum. Encountered unsupported kind at root field "${key}".`
|
|
410
|
+
);
|
|
418
411
|
}
|
|
419
412
|
}
|
|
420
413
|
function ensureMapContainers(schema) {
|
|
421
414
|
const map = new Y3.Map();
|
|
422
|
-
|
|
423
|
-
if (structural._kind !== "product") return map;
|
|
415
|
+
if (schema[KIND3] !== "product") return map;
|
|
424
416
|
for (const [key, fieldSchema] of Object.entries(
|
|
425
|
-
|
|
426
|
-
)) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
const fs = unwrapAnnotations2(fieldSchema);
|
|
433
|
-
switch (fs._kind) {
|
|
417
|
+
schema.fields
|
|
418
|
+
).sort(([a], [b]) => a.localeCompare(b))) {
|
|
419
|
+
switch (fieldSchema[KIND3]) {
|
|
420
|
+
case "text":
|
|
421
|
+
map.set(key, new Y3.Text());
|
|
422
|
+
break;
|
|
434
423
|
case "product":
|
|
435
424
|
map.set(key, ensureMapContainers(fieldSchema));
|
|
436
425
|
break;
|
|
@@ -448,19 +437,19 @@ function ensureMapContainers(schema) {
|
|
|
448
437
|
}
|
|
449
438
|
break;
|
|
450
439
|
}
|
|
440
|
+
case "counter":
|
|
441
|
+
case "set":
|
|
442
|
+
case "tree":
|
|
443
|
+
case "movable":
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Yjs substrate does not support [KIND]="${fieldSchema[KIND3]}". Supported kinds: text, product, sequence, map, scalar, sum. Encountered unsupported kind at nested field "${key}".`
|
|
446
|
+
);
|
|
451
447
|
}
|
|
452
448
|
}
|
|
453
449
|
return map;
|
|
454
450
|
}
|
|
455
|
-
function unwrapAnnotations2(schema) {
|
|
456
|
-
let s = schema;
|
|
457
|
-
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
458
|
-
s = s.schema;
|
|
459
|
-
}
|
|
460
|
-
return s;
|
|
461
|
-
}
|
|
462
451
|
|
|
463
|
-
// src/
|
|
452
|
+
// src/reader.ts
|
|
464
453
|
import * as Y4 from "yjs";
|
|
465
454
|
function extractValue(resolved) {
|
|
466
455
|
if (resolved instanceof Y4.Text) {
|
|
@@ -474,7 +463,7 @@ function extractValue(resolved) {
|
|
|
474
463
|
}
|
|
475
464
|
return resolved;
|
|
476
465
|
}
|
|
477
|
-
function
|
|
466
|
+
function yjsReader(doc, schema) {
|
|
478
467
|
const rootMap = doc.getMap("root");
|
|
479
468
|
return {
|
|
480
469
|
read(path) {
|
|
@@ -518,6 +507,7 @@ function yjsStoreReader(doc, schema) {
|
|
|
518
507
|
}
|
|
519
508
|
|
|
520
509
|
// src/version.ts
|
|
510
|
+
import { versionVectorMeet } from "@kyneta/schema";
|
|
521
511
|
import { decodeStateVector } from "yjs";
|
|
522
512
|
function uint8ArrayToBase64(bytes) {
|
|
523
513
|
let binary = "";
|
|
@@ -534,6 +524,22 @@ function base64ToUint8Array(base64) {
|
|
|
534
524
|
}
|
|
535
525
|
return bytes;
|
|
536
526
|
}
|
|
527
|
+
function encodeStateVector(map) {
|
|
528
|
+
const bytes = [];
|
|
529
|
+
function writeVarUint(value) {
|
|
530
|
+
while (value > 127) {
|
|
531
|
+
bytes.push(value & 127 | 128);
|
|
532
|
+
value >>>= 7;
|
|
533
|
+
}
|
|
534
|
+
bytes.push(value & 127);
|
|
535
|
+
}
|
|
536
|
+
writeVarUint(map.size);
|
|
537
|
+
for (const [clientId, clock] of map) {
|
|
538
|
+
writeVarUint(clientId);
|
|
539
|
+
writeVarUint(clock);
|
|
540
|
+
}
|
|
541
|
+
return new Uint8Array(bytes);
|
|
542
|
+
}
|
|
537
543
|
var YjsVersion = class _YjsVersion {
|
|
538
544
|
sv;
|
|
539
545
|
constructor(sv) {
|
|
@@ -565,9 +571,7 @@ var YjsVersion = class _YjsVersion {
|
|
|
565
571
|
*/
|
|
566
572
|
compare(other) {
|
|
567
573
|
if (!(other instanceof _YjsVersion)) {
|
|
568
|
-
throw new Error(
|
|
569
|
-
"YjsVersion can only be compared with another YjsVersion"
|
|
570
|
-
);
|
|
574
|
+
throw new Error("YjsVersion can only be compared with another YjsVersion");
|
|
571
575
|
}
|
|
572
576
|
const thisMap = decodeStateVector(this.sv);
|
|
573
577
|
const otherMap = decodeStateVector(other.sv);
|
|
@@ -593,6 +597,26 @@ var YjsVersion = class _YjsVersion {
|
|
|
593
597
|
if (hasGreater && !hasLess) return "ahead";
|
|
594
598
|
return "equal";
|
|
595
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Greatest lower bound (lattice meet) of two Yjs versions.
|
|
602
|
+
*
|
|
603
|
+
* Decodes both state vectors, computes the component-wise minimum
|
|
604
|
+
* via the shared `versionVectorMeet` utility, and encodes the result
|
|
605
|
+
* back to a Yjs state vector.
|
|
606
|
+
*
|
|
607
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
608
|
+
*/
|
|
609
|
+
meet(other) {
|
|
610
|
+
if (!(other instanceof _YjsVersion)) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
"YjsVersion can only be meet'd with another YjsVersion"
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const thisMap = decodeStateVector(this.sv);
|
|
616
|
+
const otherMap = decodeStateVector(other.sv);
|
|
617
|
+
const result = versionVectorMeet(thisMap, otherMap);
|
|
618
|
+
return new _YjsVersion(encodeStateVector(result));
|
|
619
|
+
}
|
|
596
620
|
/**
|
|
597
621
|
* Parse a serialized YjsVersion string back into a YjsVersion.
|
|
598
622
|
*
|
|
@@ -607,47 +631,24 @@ var YjsVersion = class _YjsVersion {
|
|
|
607
631
|
}
|
|
608
632
|
};
|
|
609
633
|
|
|
610
|
-
// src/yjs-escape.ts
|
|
611
|
-
import { unwrap } from "@kyneta/schema";
|
|
612
|
-
var substrateToYjsDoc = /* @__PURE__ */ new WeakMap();
|
|
613
|
-
function registerYjsSubstrate(substrate, doc) {
|
|
614
|
-
substrateToYjsDoc.set(substrate, doc);
|
|
615
|
-
}
|
|
616
|
-
function yjs(ref) {
|
|
617
|
-
let substrate;
|
|
618
|
-
try {
|
|
619
|
-
substrate = unwrap(ref);
|
|
620
|
-
} catch {
|
|
621
|
-
throw new Error(
|
|
622
|
-
"yjs() requires a ref backed by a Yjs substrate. Use a doc created by exchange.get() with a bindYjs() schema, or by createYjsDoc()."
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
const doc = substrateToYjsDoc.get(substrate);
|
|
626
|
-
if (!doc) {
|
|
627
|
-
throw new Error(
|
|
628
|
-
"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()."
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
return doc;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
634
|
// src/substrate.ts
|
|
635
635
|
var KYNETA_ORIGIN = "kyneta-prepare";
|
|
636
636
|
function createYjsSubstrate(doc, schema) {
|
|
637
637
|
const pendingChanges = [];
|
|
638
638
|
let inOurTransaction = false;
|
|
639
|
-
let
|
|
639
|
+
let pendingMergeOrigin;
|
|
640
640
|
let cachedCtx;
|
|
641
641
|
const rootMap = doc.getMap("root");
|
|
642
|
-
const reader =
|
|
642
|
+
const reader = yjsReader(doc, schema);
|
|
643
643
|
const substrate = {
|
|
644
|
-
|
|
644
|
+
[BACKING_DOC]: doc,
|
|
645
|
+
reader,
|
|
645
646
|
prepare(path, change2) {
|
|
646
647
|
if (!inOurTransaction) {
|
|
647
648
|
pendingChanges.push({ path, change: change2 });
|
|
648
649
|
}
|
|
649
650
|
},
|
|
650
|
-
onFlush(
|
|
651
|
+
onFlush(_origin) {
|
|
651
652
|
if (!inOurTransaction && pendingChanges.length > 0) {
|
|
652
653
|
inOurTransaction = true;
|
|
653
654
|
try {
|
|
@@ -671,8 +672,17 @@ function createYjsSubstrate(doc, schema) {
|
|
|
671
672
|
version() {
|
|
672
673
|
return new YjsVersion(Y5.encodeStateVector(doc));
|
|
673
674
|
},
|
|
674
|
-
|
|
675
|
+
baseVersion() {
|
|
676
|
+
return new YjsVersion(new Uint8Array([0]));
|
|
677
|
+
},
|
|
678
|
+
advance(_to) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
"advance() on a live Yjs substrate is not yet supported. Use advance() on a YjsReplica instead."
|
|
681
|
+
);
|
|
682
|
+
},
|
|
683
|
+
exportEntirety() {
|
|
675
684
|
return {
|
|
685
|
+
kind: "entirety",
|
|
676
686
|
encoding: "binary",
|
|
677
687
|
data: Y5.encodeStateAsUpdate(doc)
|
|
678
688
|
};
|
|
@@ -680,22 +690,22 @@ function createYjsSubstrate(doc, schema) {
|
|
|
680
690
|
exportSince(since) {
|
|
681
691
|
try {
|
|
682
692
|
const bytes = Y5.encodeStateAsUpdate(doc, since.sv);
|
|
683
|
-
return { encoding: "binary", data: bytes };
|
|
693
|
+
return { kind: "since", encoding: "binary", data: bytes };
|
|
684
694
|
} catch {
|
|
685
695
|
return null;
|
|
686
696
|
}
|
|
687
697
|
},
|
|
688
|
-
|
|
698
|
+
merge(payload, origin) {
|
|
689
699
|
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
690
700
|
throw new Error(
|
|
691
|
-
"YjsSubstrate.
|
|
701
|
+
"YjsSubstrate.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
|
|
692
702
|
);
|
|
693
703
|
}
|
|
694
|
-
|
|
704
|
+
pendingMergeOrigin = origin;
|
|
695
705
|
try {
|
|
696
706
|
Y5.applyUpdate(doc, payload.data, origin ?? "remote");
|
|
697
707
|
} finally {
|
|
698
|
-
|
|
708
|
+
pendingMergeOrigin = void 0;
|
|
699
709
|
}
|
|
700
710
|
}
|
|
701
711
|
};
|
|
@@ -703,11 +713,11 @@ function createYjsSubstrate(doc, schema) {
|
|
|
703
713
|
if (transaction.origin === KYNETA_ORIGIN) {
|
|
704
714
|
return;
|
|
705
715
|
}
|
|
706
|
-
const ops = eventsToOps(events);
|
|
716
|
+
const ops = eventsToOps(events, schema);
|
|
707
717
|
if (ops.length === 0) {
|
|
708
718
|
return;
|
|
709
719
|
}
|
|
710
|
-
const origin =
|
|
720
|
+
const origin = pendingMergeOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
|
|
711
721
|
const ctx = substrate.context();
|
|
712
722
|
inOurTransaction = true;
|
|
713
723
|
try {
|
|
@@ -716,25 +726,103 @@ function createYjsSubstrate(doc, schema) {
|
|
|
716
726
|
inOurTransaction = false;
|
|
717
727
|
}
|
|
718
728
|
});
|
|
719
|
-
registerYjsSubstrate(substrate, doc);
|
|
720
729
|
return substrate;
|
|
721
730
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
731
|
+
function createYjsReplica(doc) {
|
|
732
|
+
let currentDoc = doc;
|
|
733
|
+
let currentBase = new YjsVersion(
|
|
734
|
+
Y5.encodeStateVector(new Y5.Doc())
|
|
735
|
+
);
|
|
736
|
+
return {
|
|
737
|
+
get [BACKING_DOC]() {
|
|
738
|
+
return currentDoc;
|
|
739
|
+
},
|
|
740
|
+
version() {
|
|
741
|
+
return new YjsVersion(Y5.encodeStateVector(currentDoc));
|
|
742
|
+
},
|
|
743
|
+
baseVersion() {
|
|
744
|
+
return currentBase;
|
|
745
|
+
},
|
|
746
|
+
advance(to) {
|
|
747
|
+
const baseCmp = currentBase.compare(to);
|
|
748
|
+
if (baseCmp === "ahead") {
|
|
749
|
+
throw new Error("advance(): target is behind base version");
|
|
750
|
+
}
|
|
751
|
+
const currentCmp = to.compare(this.version());
|
|
752
|
+
if (currentCmp === "ahead") {
|
|
753
|
+
throw new Error("advance(): target is ahead of current version");
|
|
754
|
+
}
|
|
755
|
+
if (currentCmp !== "equal") return;
|
|
756
|
+
const update = Y5.encodeStateAsUpdate(currentDoc);
|
|
757
|
+
const newDoc = new Y5.Doc();
|
|
758
|
+
Y5.applyUpdate(newDoc, update);
|
|
759
|
+
currentDoc = newDoc;
|
|
760
|
+
currentBase = new YjsVersion(Y5.encodeStateVector(currentDoc));
|
|
761
|
+
},
|
|
762
|
+
exportEntirety() {
|
|
763
|
+
return {
|
|
764
|
+
kind: "entirety",
|
|
765
|
+
encoding: "binary",
|
|
766
|
+
data: Y5.encodeStateAsUpdate(currentDoc)
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
exportSince(since) {
|
|
770
|
+
try {
|
|
771
|
+
const bytes = Y5.encodeStateAsUpdate(currentDoc, since.sv);
|
|
772
|
+
return { kind: "since", encoding: "binary", data: bytes };
|
|
773
|
+
} catch {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
merge(payload, _origin) {
|
|
778
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
"YjsReplica.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
Y5.applyUpdate(currentDoc, payload.data);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
var yjsReplicaFactory = {
|
|
788
|
+
replicaType: ["yjs", 1, 0],
|
|
789
|
+
createEmpty() {
|
|
790
|
+
return createYjsReplica(new Y5.Doc());
|
|
727
791
|
},
|
|
728
|
-
|
|
792
|
+
fromEntirety(payload) {
|
|
729
793
|
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
730
794
|
throw new Error(
|
|
731
|
-
"
|
|
795
|
+
"YjsReplicaFactory.fromEntirety only supports binary-encoded payloads"
|
|
732
796
|
);
|
|
733
797
|
}
|
|
734
798
|
const doc = new Y5.Doc();
|
|
735
799
|
Y5.applyUpdate(doc, payload.data);
|
|
800
|
+
return createYjsReplica(doc);
|
|
801
|
+
},
|
|
802
|
+
parseVersion(serialized) {
|
|
803
|
+
return YjsVersion.parse(serialized);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
var yjsSubstrateFactory = {
|
|
807
|
+
replica: yjsReplicaFactory,
|
|
808
|
+
createReplica() {
|
|
809
|
+
return createYjsReplica(new Y5.Doc());
|
|
810
|
+
},
|
|
811
|
+
upgrade(replica, schema) {
|
|
812
|
+
const doc = replica[BACKING_DOC];
|
|
813
|
+
ensureContainers(doc, schema, true);
|
|
814
|
+
return createYjsSubstrate(doc, schema);
|
|
815
|
+
},
|
|
816
|
+
create(schema) {
|
|
817
|
+
const doc = new Y5.Doc();
|
|
818
|
+
ensureContainers(doc, schema);
|
|
736
819
|
return createYjsSubstrate(doc, schema);
|
|
737
820
|
},
|
|
821
|
+
fromEntirety(payload, schema) {
|
|
822
|
+
const replica = this.createReplica();
|
|
823
|
+
replica.merge(payload);
|
|
824
|
+
return this.upgrade(replica, schema);
|
|
825
|
+
},
|
|
738
826
|
parseVersion(serialized) {
|
|
739
827
|
return YjsVersion.parse(serialized);
|
|
740
828
|
}
|
|
@@ -746,13 +834,13 @@ function getSubstrate(doc) {
|
|
|
746
834
|
const s = substrates.get(doc);
|
|
747
835
|
if (!s) {
|
|
748
836
|
throw new Error(
|
|
749
|
-
"version/
|
|
837
|
+
"version/exportEntirety/merge called on an object without a YjsSubstrate. Use a doc created by createYjsDoc() or createYjsDocFromEntirety()."
|
|
750
838
|
);
|
|
751
839
|
}
|
|
752
840
|
return s;
|
|
753
841
|
}
|
|
754
842
|
function registerDoc(schema, substrate) {
|
|
755
|
-
const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(
|
|
843
|
+
const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(observation).done();
|
|
756
844
|
substrates.set(doc, substrate);
|
|
757
845
|
registerSubstrate(doc, substrate);
|
|
758
846
|
return doc;
|
|
@@ -767,30 +855,29 @@ var createYjsDoc = (schema, doc) => {
|
|
|
767
855
|
}
|
|
768
856
|
return registerDoc(schema, yjsSubstrateFactory.create(schema));
|
|
769
857
|
};
|
|
770
|
-
var
|
|
858
|
+
var createYjsDocFromEntirety = (schema, payload) => registerDoc(schema, yjsSubstrateFactory.fromEntirety(payload, schema));
|
|
771
859
|
|
|
772
860
|
// src/sync.ts
|
|
773
861
|
function version(doc) {
|
|
774
862
|
return getSubstrate(doc).version();
|
|
775
863
|
}
|
|
776
|
-
function
|
|
777
|
-
return getSubstrate(doc).
|
|
864
|
+
function exportEntirety(doc) {
|
|
865
|
+
return getSubstrate(doc).exportEntirety();
|
|
778
866
|
}
|
|
779
867
|
function exportSince(doc, since) {
|
|
780
868
|
return getSubstrate(doc).exportSince(since);
|
|
781
869
|
}
|
|
782
|
-
function
|
|
783
|
-
getSubstrate(doc).
|
|
870
|
+
function merge(doc, payload, origin) {
|
|
871
|
+
getSubstrate(doc).merge(payload, origin);
|
|
784
872
|
}
|
|
785
873
|
|
|
786
|
-
// src/index.ts
|
|
787
|
-
import { applyChanges, change } from "@kyneta/schema";
|
|
788
|
-
import { subscribe, subscribeNode } from "@kyneta/schema";
|
|
789
|
-
import { Schema } from "@kyneta/schema";
|
|
790
|
-
import { Schema as Schema2 } from "@kyneta/schema";
|
|
791
|
-
|
|
792
874
|
// src/bind-yjs.ts
|
|
793
|
-
import {
|
|
875
|
+
import {
|
|
876
|
+
BACKING_DOC as BACKING_DOC2,
|
|
877
|
+
createSubstrateNamespace,
|
|
878
|
+
STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2,
|
|
879
|
+
unwrap
|
|
880
|
+
} from "@kyneta/schema";
|
|
794
881
|
import * as Y6 from "yjs";
|
|
795
882
|
function hashPeerId(peerId) {
|
|
796
883
|
let hash = 2166136261;
|
|
@@ -798,68 +885,92 @@ function hashPeerId(peerId) {
|
|
|
798
885
|
hash ^= peerId.charCodeAt(i);
|
|
799
886
|
hash = Math.imul(hash, 16777619);
|
|
800
887
|
}
|
|
801
|
-
|
|
888
|
+
const result = hash >>> 0;
|
|
889
|
+
return result === STRUCTURAL_YJS_CLIENT_ID2 ? 1 : result;
|
|
802
890
|
}
|
|
803
891
|
function createYjsFactory(peerId) {
|
|
804
892
|
const numericClientId = hashPeerId(peerId);
|
|
805
893
|
return {
|
|
806
|
-
|
|
807
|
-
|
|
894
|
+
replica: yjsReplicaFactory,
|
|
895
|
+
createReplica() {
|
|
896
|
+
return createYjsReplica(new Y6.Doc());
|
|
897
|
+
},
|
|
898
|
+
upgrade(replica, schema) {
|
|
899
|
+
const doc = replica[BACKING_DOC2];
|
|
808
900
|
doc.clientID = numericClientId;
|
|
809
|
-
ensureContainers(doc, schema);
|
|
901
|
+
ensureContainers(doc, schema, true);
|
|
810
902
|
return createYjsSubstrate(doc, schema);
|
|
811
903
|
},
|
|
812
|
-
|
|
813
|
-
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
814
|
-
throw new Error(
|
|
815
|
-
"YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads"
|
|
816
|
-
);
|
|
817
|
-
}
|
|
904
|
+
create(schema) {
|
|
818
905
|
const doc = new Y6.Doc();
|
|
819
906
|
doc.clientID = numericClientId;
|
|
820
|
-
|
|
907
|
+
ensureContainers(doc, schema);
|
|
821
908
|
return createYjsSubstrate(doc, schema);
|
|
822
909
|
},
|
|
910
|
+
fromEntirety(payload, schema) {
|
|
911
|
+
const replica = this.createReplica();
|
|
912
|
+
replica.merge(payload);
|
|
913
|
+
return this.upgrade(replica, schema);
|
|
914
|
+
},
|
|
823
915
|
parseVersion(serialized) {
|
|
824
916
|
return YjsVersion.parse(serialized);
|
|
825
917
|
}
|
|
826
918
|
};
|
|
827
919
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
920
|
+
var yjs = {
|
|
921
|
+
...createSubstrateNamespace({
|
|
922
|
+
strategies: {
|
|
923
|
+
collaborative: {
|
|
924
|
+
factory: (ctx) => createYjsFactory(ctx.peerId),
|
|
925
|
+
replicaFactory: yjsReplicaFactory
|
|
926
|
+
},
|
|
927
|
+
ephemeral: {
|
|
928
|
+
factory: (ctx) => createYjsFactory(ctx.peerId),
|
|
929
|
+
replicaFactory: yjsReplicaFactory
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
defaultStrategy: "collaborative"
|
|
933
|
+
}),
|
|
934
|
+
unwrap(ref) {
|
|
935
|
+
let substrate;
|
|
936
|
+
try {
|
|
937
|
+
substrate = unwrap(ref);
|
|
938
|
+
} catch {
|
|
939
|
+
throw new Error(
|
|
940
|
+
"yjs.unwrap() requires a ref backed by a Yjs substrate. Use a doc created by exchange.get() with a yjs.bind() schema, or by createYjsDoc()."
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const doc = substrate[BACKING_DOC2];
|
|
944
|
+
if (!doc || typeof doc !== "object" || typeof doc.getMap !== "function" || typeof doc.clientID !== "number") {
|
|
945
|
+
throw new Error(
|
|
946
|
+
"yjs.unwrap() 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 yjs.bind() schema or createYjsDoc()."
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return doc;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
840
952
|
export {
|
|
841
953
|
Schema,
|
|
842
954
|
YjsVersion,
|
|
843
955
|
applyChangeToYjs,
|
|
844
956
|
applyChanges,
|
|
845
|
-
bindYjs,
|
|
846
957
|
change,
|
|
847
958
|
createYjsDoc,
|
|
848
|
-
|
|
959
|
+
createYjsDocFromEntirety,
|
|
849
960
|
createYjsSubstrate,
|
|
850
961
|
ensureContainers,
|
|
851
962
|
eventsToOps,
|
|
963
|
+
exportEntirety,
|
|
852
964
|
exportSince,
|
|
853
|
-
|
|
854
|
-
importDelta,
|
|
965
|
+
merge,
|
|
855
966
|
resolveYjsType,
|
|
856
967
|
stepIntoYjs,
|
|
857
968
|
subscribe,
|
|
858
969
|
subscribeNode,
|
|
859
|
-
text,
|
|
860
970
|
version,
|
|
861
971
|
yjs,
|
|
862
|
-
|
|
972
|
+
yjsReader,
|
|
973
|
+
yjsReplicaFactory,
|
|
863
974
|
yjsSubstrateFactory
|
|
864
975
|
};
|
|
865
976
|
//# sourceMappingURL=index.js.map
|