@kyneta/yjs-schema 1.1.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.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
 
10
10
  // src/create.ts
11
11
  import {
12
- changefeed,
13
12
  interpret,
13
+ observation,
14
14
  readable,
15
15
  registerSubstrate,
16
16
  writable
@@ -21,7 +21,7 @@ import { BACKING_DOC, buildWritableContext, executeBatch } from "@kyneta/schema"
21
21
  import * as Y5 from "yjs";
22
22
 
23
23
  // src/change-mapping.ts
24
- import { advanceSchema as advanceSchema2, expandMapOpsToLeaves, RawPath } from "@kyneta/schema";
24
+ import { advanceSchema as advanceSchema2, expandMapOpsToLeaves, KIND as KIND2, RawPath } from "@kyneta/schema";
25
25
  import * as Y2 from "yjs";
26
26
 
27
27
  // src/yjs-resolve.ts
@@ -43,10 +43,6 @@ function stepIntoYjs(current, segment) {
43
43
  function resolveYjsType(rootMap, rootSchema, path) {
44
44
  let current = rootMap;
45
45
  let schema = rootSchema;
46
- let rootProduct = rootSchema;
47
- while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
48
- rootProduct = rootProduct.schema;
49
- }
50
46
  for (let i = 0; i < path.length; i++) {
51
47
  const seg = path.segments[i];
52
48
  const nextSchema = advanceSchema(schema, seg);
@@ -73,11 +69,11 @@ function applyChangeToYjs(rootMap, rootSchema, path, change2) {
73
69
  return;
74
70
  case "increment":
75
71
  throw new Error(
76
- `Yjs substrate does not support counter annotations. Use Schema.number() with ReplaceChange instead. Attempted IncrementChange with amount=${change2.amount} at path [${pathToString(path)}].`
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)}].`
77
73
  );
78
74
  case "tree":
79
75
  throw new Error(
80
- `Yjs substrate does not support tree annotations. Yjs has no native tree type. Attempted TreeChange at path [${pathToString(path)}].`
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)}].`
81
77
  );
82
78
  default:
83
79
  throw new Error(
@@ -167,29 +163,25 @@ function applyReplaceChange(rootMap, rootSchema, path, change2) {
167
163
  }
168
164
  function maybeCreateSharedType(value, schema) {
169
165
  if (schema === void 0) return value;
170
- const structural = unwrapAnnotations(schema);
171
- const tag = schema._kind === "annotated" ? schema.tag : void 0;
172
- if (tag === "text") {
173
- const text2 = new Y2.Text();
174
- if (typeof value === "string" && value.length > 0) {
175
- text2.insert(0, value);
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;
176
174
  }
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
175
  case "product": {
184
176
  if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
185
177
  return value;
186
178
  }
187
- return createStructuredMap(value, structural);
179
+ return createStructuredMap(value, schema);
188
180
  }
189
181
  case "sequence": {
190
182
  if (!Array.isArray(value)) return value;
191
183
  const arr = new Y2.Array();
192
- const itemSchema = structural.item;
184
+ const itemSchema = schema.item;
193
185
  const items = value.map(
194
186
  (item) => maybeCreateSharedType(item, itemSchema)
195
187
  );
@@ -201,20 +193,28 @@ function maybeCreateSharedType(value, schema) {
201
193
  return value;
202
194
  }
203
195
  const map = new Y2.Map();
204
- const valueSchema = structural.item;
196
+ const valueSchema = schema.item;
205
197
  for (const [k, v] of Object.entries(value)) {
206
198
  map.set(k, maybeCreateSharedType(v, valueSchema));
207
199
  }
208
200
  return map;
209
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
+ );
210
211
  default:
211
212
  return value;
212
213
  }
213
214
  }
214
215
  function createStructuredMap(obj, productSchema) {
215
216
  const map = new Y2.Map();
216
- const structural = unwrapAnnotations(productSchema);
217
- if (structural._kind !== "product") {
217
+ if (productSchema[KIND2] !== "product") {
218
218
  for (const [key, val] of Object.entries(obj)) {
219
219
  map.set(key, val);
220
220
  }
@@ -222,22 +222,21 @@ function createStructuredMap(obj, productSchema) {
222
222
  }
223
223
  for (const [key, val] of Object.entries(obj)) {
224
224
  if (val === void 0) continue;
225
- const fieldSchema = structural.fields[key];
225
+ const fieldSchema = productSchema.fields[key];
226
226
  const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val;
227
227
  map.set(key, yjsVal);
228
228
  }
229
229
  for (const [key, fieldSchema] of Object.entries(
230
- structural.fields
230
+ productSchema.fields
231
231
  )) {
232
232
  if (key in obj) continue;
233
- const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
234
- if (tag === "text") {
233
+ if (fieldSchema[KIND2] === "text") {
235
234
  map.set(key, new Y2.Text());
236
235
  }
237
236
  }
238
237
  return map;
239
238
  }
240
- function eventsToOps(events) {
239
+ function eventsToOps(events, schema) {
241
240
  const ops = [];
242
241
  for (const event of events) {
243
242
  const kynetaPath = yjsPathToKynetaPath(event.path);
@@ -246,7 +245,7 @@ function eventsToOps(events) {
246
245
  ops.push({ path: kynetaPath, change: change2 });
247
246
  }
248
247
  }
249
- return expandMapOpsToLeaves(ops);
248
+ return expandMapOpsToLeaves(ops, schema);
250
249
  }
251
250
  function yjsPathToKynetaPath(yjsPath) {
252
251
  let path = RawPath.empty;
@@ -329,13 +328,6 @@ function extractEventValue(value) {
329
328
  if (value instanceof Y2.Text) return value.toJSON();
330
329
  return value;
331
330
  }
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
331
  function resolveSchemaAtPath(rootSchema, path) {
340
332
  let schema = rootSchema;
341
333
  for (const seg of path.segments) {
@@ -344,16 +336,19 @@ function resolveSchemaAtPath(rootSchema, path) {
344
336
  return schema;
345
337
  }
346
338
  function getItemSchema(schema) {
347
- const structural = unwrapAnnotations(schema);
348
- return structural._kind === "sequence" ? structural.item : void 0;
339
+ if (schema[KIND2] === "sequence") return schema.item;
340
+ if (schema[KIND2] === "movable") return schema.item;
341
+ return void 0;
349
342
  }
350
343
  function getFieldSchema(schema, key) {
351
- const structural = unwrapAnnotations(schema);
352
- if (structural._kind === "product") {
353
- return structural.fields[key];
344
+ if (schema[KIND2] === "product") {
345
+ return schema.fields[key];
346
+ }
347
+ if (schema[KIND2] === "map") {
348
+ return schema.item;
354
349
  }
355
- if (structural._kind === "map") {
356
- return structural.item;
350
+ if (schema[KIND2] === "set") {
351
+ return schema.item;
357
352
  }
358
353
  return void 0;
359
354
  }
@@ -362,22 +357,18 @@ function pathToString(path) {
362
357
  }
363
358
 
364
359
  // src/populate.ts
365
- import { STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
360
+ import { KIND as KIND3, STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
366
361
  import * as Y3 from "yjs";
367
362
  function ensureContainers(doc, schema, conditional = false) {
368
363
  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") {
364
+ if (schema[KIND3] !== "product") {
374
365
  return;
375
366
  }
376
367
  const savedClientID = doc.clientID;
377
368
  doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
378
369
  try {
379
370
  doc.transact(() => {
380
- for (const [key, fieldSchema] of Object.entries(rootProduct.fields).sort(
371
+ for (const [key, fieldSchema] of Object.entries(schema.fields).sort(
381
372
  ([a], [b]) => a.localeCompare(b)
382
373
  )) {
383
374
  if (conditional && rootMap.has(key)) continue;
@@ -389,28 +380,12 @@ function ensureContainers(doc, schema, conditional = false) {
389
380
  }
390
381
  }
391
382
  function ensureRootField(rootMap, key, fieldSchema) {
392
- const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
393
- switch (tag) {
383
+ switch (fieldSchema[KIND3]) {
394
384
  case "text":
395
385
  rootMap.set(key, new Y3.Text());
396
386
  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
387
  case "product":
413
- rootMap.set(key, ensureMapContainers(structural));
388
+ rootMap.set(key, ensureMapContainers(fieldSchema));
414
389
  return;
415
390
  case "sequence":
416
391
  rootMap.set(key, new Y3.Array());
@@ -426,22 +401,25 @@ function ensureRootField(rootMap, key, fieldSchema) {
426
401
  }
427
402
  return;
428
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
+ );
429
411
  }
430
412
  }
431
413
  function ensureMapContainers(schema) {
432
414
  const map = new Y3.Map();
433
- const structural = unwrapAnnotations2(schema);
434
- if (structural._kind !== "product") return map;
415
+ if (schema[KIND3] !== "product") return map;
435
416
  for (const [key, fieldSchema] of Object.entries(
436
- structural.fields
417
+ schema.fields
437
418
  ).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) {
419
+ switch (fieldSchema[KIND3]) {
420
+ case "text":
421
+ map.set(key, new Y3.Text());
422
+ break;
445
423
  case "product":
446
424
  map.set(key, ensureMapContainers(fieldSchema));
447
425
  break;
@@ -459,17 +437,17 @@ function ensureMapContainers(schema) {
459
437
  }
460
438
  break;
461
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
+ );
462
447
  }
463
448
  }
464
449
  return map;
465
450
  }
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
451
 
474
452
  // src/reader.ts
475
453
  import * as Y4 from "yjs";
@@ -529,6 +507,7 @@ function yjsReader(doc, schema) {
529
507
  }
530
508
 
531
509
  // src/version.ts
510
+ import { versionVectorMeet } from "@kyneta/schema";
532
511
  import { decodeStateVector } from "yjs";
533
512
  function uint8ArrayToBase64(bytes) {
534
513
  let binary = "";
@@ -545,6 +524,22 @@ function base64ToUint8Array(base64) {
545
524
  }
546
525
  return bytes;
547
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
+ }
548
543
  var YjsVersion = class _YjsVersion {
549
544
  sv;
550
545
  constructor(sv) {
@@ -602,6 +597,26 @@ var YjsVersion = class _YjsVersion {
602
597
  if (hasGreater && !hasLess) return "ahead";
603
598
  return "equal";
604
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
+ }
605
620
  /**
606
621
  * Parse a serialized YjsVersion string back into a YjsVersion.
607
622
  *
@@ -633,7 +648,7 @@ function createYjsSubstrate(doc, schema) {
633
648
  pendingChanges.push({ path, change: change2 });
634
649
  }
635
650
  },
636
- onFlush(origin) {
651
+ onFlush(_origin) {
637
652
  if (!inOurTransaction && pendingChanges.length > 0) {
638
653
  inOurTransaction = true;
639
654
  try {
@@ -657,6 +672,14 @@ function createYjsSubstrate(doc, schema) {
657
672
  version() {
658
673
  return new YjsVersion(Y5.encodeStateVector(doc));
659
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
+ },
660
683
  exportEntirety() {
661
684
  return {
662
685
  kind: "entirety",
@@ -690,7 +713,7 @@ function createYjsSubstrate(doc, schema) {
690
713
  if (transaction.origin === KYNETA_ORIGIN) {
691
714
  return;
692
715
  }
693
- const ops = eventsToOps(events);
716
+ const ops = eventsToOps(events, schema);
694
717
  if (ops.length === 0) {
695
718
  return;
696
719
  }
@@ -706,21 +729,46 @@ function createYjsSubstrate(doc, schema) {
706
729
  return substrate;
707
730
  }
708
731
  function createYjsReplica(doc) {
732
+ let currentDoc = doc;
733
+ let currentBase = new YjsVersion(
734
+ Y5.encodeStateVector(new Y5.Doc())
735
+ );
709
736
  return {
710
- [BACKING_DOC]: doc,
737
+ get [BACKING_DOC]() {
738
+ return currentDoc;
739
+ },
711
740
  version() {
712
- return new YjsVersion(Y5.encodeStateVector(doc));
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));
713
761
  },
714
762
  exportEntirety() {
715
763
  return {
716
764
  kind: "entirety",
717
765
  encoding: "binary",
718
- data: Y5.encodeStateAsUpdate(doc)
766
+ data: Y5.encodeStateAsUpdate(currentDoc)
719
767
  };
720
768
  },
721
769
  exportSince(since) {
722
770
  try {
723
- const bytes = Y5.encodeStateAsUpdate(doc, since.sv);
771
+ const bytes = Y5.encodeStateAsUpdate(currentDoc, since.sv);
724
772
  return { kind: "since", encoding: "binary", data: bytes };
725
773
  } catch {
726
774
  return null;
@@ -732,7 +780,7 @@ function createYjsReplica(doc) {
732
780
  "YjsReplica.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
733
781
  );
734
782
  }
735
- Y5.applyUpdate(doc, payload.data);
783
+ Y5.applyUpdate(currentDoc, payload.data);
736
784
  }
737
785
  };
738
786
  }
@@ -792,7 +840,7 @@ function getSubstrate(doc) {
792
840
  return s;
793
841
  }
794
842
  function registerDoc(schema, substrate) {
795
- const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(changefeed).done();
843
+ const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(observation).done();
796
844
  substrates.set(doc, substrate);
797
845
  registerSubstrate(doc, substrate);
798
846
  return doc;
@@ -823,11 +871,13 @@ function merge(doc, payload, origin) {
823
871
  getSubstrate(doc).merge(payload, origin);
824
872
  }
825
873
 
826
- // src/index.ts
827
- import { Schema as Schema2 } from "@kyneta/schema";
828
-
829
874
  // src/bind-yjs.ts
830
- import { BACKING_DOC as BACKING_DOC2, bind, STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2 } from "@kyneta/schema";
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";
831
881
  import * as Y6 from "yjs";
832
882
  function hashPeerId(peerId) {
833
883
  let hash = 2166136261;
@@ -867,44 +917,43 @@ function createYjsFactory(peerId) {
867
917
  }
868
918
  };
869
919
  }
870
- function bindYjs(schema) {
871
- return bind({
872
- schema,
873
- factory: (ctx) => createYjsFactory(ctx.peerId),
874
- strategy: "causal"
875
- });
876
- }
877
-
878
- // src/yjs-escape.ts
879
- import { BACKING_DOC as BACKING_DOC3, unwrap } from "@kyneta/schema";
880
- function yjs(ref) {
881
- let substrate;
882
- try {
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
- );
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;
894
950
  }
895
- return doc;
896
- }
897
-
898
- // src/index.ts
899
- function text() {
900
- return Schema2.annotated("text");
901
- }
951
+ };
902
952
  export {
903
953
  Schema,
904
954
  YjsVersion,
905
955
  applyChangeToYjs,
906
956
  applyChanges,
907
- bindYjs,
908
957
  change,
909
958
  createYjsDoc,
910
959
  createYjsDocFromEntirety,
@@ -918,10 +967,10 @@ export {
918
967
  stepIntoYjs,
919
968
  subscribe,
920
969
  subscribeNode,
921
- text,
922
970
  version,
923
971
  yjs,
924
972
  yjsReader,
973
+ yjsReplicaFactory,
925
974
  yjsSubstrateFactory
926
975
  };
927
976
  //# sourceMappingURL=index.js.map