@kyneta/yjs-schema 1.0.0 → 1.1.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
@@ -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 { interpret, registerSubstrate } from "@kyneta/schema";
3
- import { changefeed, readable, writable } from "@kyneta/schema";
11
+ import {
12
+ changefeed,
13
+ interpret,
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, RawPath } from "@kyneta/schema";
12
25
  import * as Y2 from "yjs";
13
26
 
14
27
  // src/yjs-resolve.ts
@@ -23,9 +36,7 @@ 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
  }
@@ -166,19 +177,14 @@ function maybeCreateSharedType(value, schema) {
166
177
  return text2;
167
178
  }
168
179
  if (tag === "counter" || tag === "movable" || tag === "tree") {
169
- throw new Error(
170
- `Yjs substrate does not support "${tag}" annotations.`
171
- );
180
+ throw new Error(`Yjs substrate does not support "${tag}" annotations.`);
172
181
  }
173
182
  switch (structural._kind) {
174
183
  case "product": {
175
184
  if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
176
185
  return value;
177
186
  }
178
- return createStructuredMap(
179
- value,
180
- structural
181
- );
187
+ return createStructuredMap(value, structural);
182
188
  }
183
189
  case "sequence": {
184
190
  if (!Array.isArray(value)) return value;
@@ -196,9 +202,7 @@ function maybeCreateSharedType(value, schema) {
196
202
  }
197
203
  const map = new Y2.Map();
198
204
  const valueSchema = structural.item;
199
- for (const [k, v] of Object.entries(
200
- value
201
- )) {
205
+ for (const [k, v] of Object.entries(value)) {
202
206
  map.set(k, maybeCreateSharedType(v, valueSchema));
203
207
  }
204
208
  return map;
@@ -302,18 +306,16 @@ function mapEventToChange(event) {
302
306
  let hasSet = false;
303
307
  let hasDelete = false;
304
308
  const target = event.target;
305
- event.changes.keys.forEach(
306
- (change2, key) => {
307
- if (change2.action === "add" || change2.action === "update") {
308
- const value = target.get(key);
309
- set[key] = extractEventValue(value);
310
- hasSet = true;
311
- } else if (change2.action === "delete") {
312
- deleteKeys.push(key);
313
- hasDelete = true;
314
- }
309
+ event.changes.keys.forEach((change2, key) => {
310
+ if (change2.action === "add" || change2.action === "update") {
311
+ const value = target.get(key);
312
+ set[key] = extractEventValue(value);
313
+ hasSet = true;
314
+ } else if (change2.action === "delete") {
315
+ deleteKeys.push(key);
316
+ hasDelete = true;
315
317
  }
316
- );
318
+ });
317
319
  if (!hasSet && !hasDelete) return null;
318
320
  return {
319
321
  type: "map",
@@ -360,9 +362,9 @@ function pathToString(path) {
360
362
  }
361
363
 
362
364
  // src/populate.ts
363
- import { Zero } from "@kyneta/schema";
365
+ import { STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema";
364
366
  import * as Y3 from "yjs";
365
- function ensureContainers(doc, schema) {
367
+ function ensureContainers(doc, schema, conditional = false) {
366
368
  const rootMap = doc.getMap("root");
367
369
  let rootProduct = schema;
368
370
  while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
@@ -371,11 +373,20 @@ function ensureContainers(doc, schema) {
371
373
  if (rootProduct._kind !== "product") {
372
374
  return;
373
375
  }
374
- doc.transact(() => {
375
- for (const [key, fieldSchema] of Object.entries(rootProduct.fields)) {
376
- ensureRootField(rootMap, key, fieldSchema);
377
- }
378
- });
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
+ }
379
390
  }
380
391
  function ensureRootField(rootMap, key, fieldSchema) {
381
392
  const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
@@ -423,7 +434,7 @@ function ensureMapContainers(schema) {
423
434
  if (structural._kind !== "product") return map;
424
435
  for (const [key, fieldSchema] of Object.entries(
425
436
  structural.fields
426
- )) {
437
+ ).sort(([a], [b]) => a.localeCompare(b))) {
427
438
  const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
428
439
  if (tag === "text") {
429
440
  map.set(key, new Y3.Text());
@@ -460,7 +471,7 @@ function unwrapAnnotations2(schema) {
460
471
  return s;
461
472
  }
462
473
 
463
- // src/store-reader.ts
474
+ // src/reader.ts
464
475
  import * as Y4 from "yjs";
465
476
  function extractValue(resolved) {
466
477
  if (resolved instanceof Y4.Text) {
@@ -474,7 +485,7 @@ function extractValue(resolved) {
474
485
  }
475
486
  return resolved;
476
487
  }
477
- function yjsStoreReader(doc, schema) {
488
+ function yjsReader(doc, schema) {
478
489
  const rootMap = doc.getMap("root");
479
490
  return {
480
491
  read(path) {
@@ -565,9 +576,7 @@ var YjsVersion = class _YjsVersion {
565
576
  */
566
577
  compare(other) {
567
578
  if (!(other instanceof _YjsVersion)) {
568
- throw new Error(
569
- "YjsVersion can only be compared with another YjsVersion"
570
- );
579
+ throw new Error("YjsVersion can only be compared with another YjsVersion");
571
580
  }
572
581
  const thisMap = decodeStateVector(this.sv);
573
582
  const otherMap = decodeStateVector(other.sv);
@@ -607,41 +616,18 @@ var YjsVersion = class _YjsVersion {
607
616
  }
608
617
  };
609
618
 
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
619
  // src/substrate.ts
635
620
  var KYNETA_ORIGIN = "kyneta-prepare";
636
621
  function createYjsSubstrate(doc, schema) {
637
622
  const pendingChanges = [];
638
623
  let inOurTransaction = false;
639
- let pendingImportOrigin;
624
+ let pendingMergeOrigin;
640
625
  let cachedCtx;
641
626
  const rootMap = doc.getMap("root");
642
- const reader = yjsStoreReader(doc, schema);
627
+ const reader = yjsReader(doc, schema);
643
628
  const substrate = {
644
- store: reader,
629
+ [BACKING_DOC]: doc,
630
+ reader,
645
631
  prepare(path, change2) {
646
632
  if (!inOurTransaction) {
647
633
  pendingChanges.push({ path, change: change2 });
@@ -671,8 +657,9 @@ function createYjsSubstrate(doc, schema) {
671
657
  version() {
672
658
  return new YjsVersion(Y5.encodeStateVector(doc));
673
659
  },
674
- exportSnapshot() {
660
+ exportEntirety() {
675
661
  return {
662
+ kind: "entirety",
676
663
  encoding: "binary",
677
664
  data: Y5.encodeStateAsUpdate(doc)
678
665
  };
@@ -680,22 +667,22 @@ function createYjsSubstrate(doc, schema) {
680
667
  exportSince(since) {
681
668
  try {
682
669
  const bytes = Y5.encodeStateAsUpdate(doc, since.sv);
683
- return { encoding: "binary", data: bytes };
670
+ return { kind: "since", encoding: "binary", data: bytes };
684
671
  } catch {
685
672
  return null;
686
673
  }
687
674
  },
688
- importDelta(payload, origin) {
675
+ merge(payload, origin) {
689
676
  if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
690
677
  throw new Error(
691
- "YjsSubstrate.importDelta only supports binary-encoded payloads"
678
+ "YjsSubstrate.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
692
679
  );
693
680
  }
694
- pendingImportOrigin = origin;
681
+ pendingMergeOrigin = origin;
695
682
  try {
696
683
  Y5.applyUpdate(doc, payload.data, origin ?? "remote");
697
684
  } finally {
698
- pendingImportOrigin = void 0;
685
+ pendingMergeOrigin = void 0;
699
686
  }
700
687
  }
701
688
  };
@@ -707,7 +694,7 @@ function createYjsSubstrate(doc, schema) {
707
694
  if (ops.length === 0) {
708
695
  return;
709
696
  }
710
- const origin = pendingImportOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
697
+ const origin = pendingMergeOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
711
698
  const ctx = substrate.context();
712
699
  inOurTransaction = true;
713
700
  try {
@@ -716,25 +703,78 @@ function createYjsSubstrate(doc, schema) {
716
703
  inOurTransaction = false;
717
704
  }
718
705
  });
719
- registerYjsSubstrate(substrate, doc);
720
706
  return substrate;
721
707
  }
722
- var yjsSubstrateFactory = {
723
- create(schema) {
724
- const doc = new Y5.Doc();
725
- ensureContainers(doc, schema);
726
- return createYjsSubstrate(doc, schema);
708
+ function createYjsReplica(doc) {
709
+ return {
710
+ [BACKING_DOC]: doc,
711
+ version() {
712
+ return new YjsVersion(Y5.encodeStateVector(doc));
713
+ },
714
+ exportEntirety() {
715
+ return {
716
+ kind: "entirety",
717
+ encoding: "binary",
718
+ data: Y5.encodeStateAsUpdate(doc)
719
+ };
720
+ },
721
+ exportSince(since) {
722
+ try {
723
+ const bytes = Y5.encodeStateAsUpdate(doc, since.sv);
724
+ return { kind: "since", encoding: "binary", data: bytes };
725
+ } catch {
726
+ return null;
727
+ }
728
+ },
729
+ merge(payload, _origin) {
730
+ if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
731
+ throw new Error(
732
+ "YjsReplica.merge expects binary-encoded payloads. If you recently switched CRDT backends, stale clients may be sending incompatible data."
733
+ );
734
+ }
735
+ Y5.applyUpdate(doc, payload.data);
736
+ }
737
+ };
738
+ }
739
+ var yjsReplicaFactory = {
740
+ replicaType: ["yjs", 1, 0],
741
+ createEmpty() {
742
+ return createYjsReplica(new Y5.Doc());
727
743
  },
728
- fromSnapshot(payload, schema) {
744
+ fromEntirety(payload) {
729
745
  if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
730
746
  throw new Error(
731
- "YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads"
747
+ "YjsReplicaFactory.fromEntirety only supports binary-encoded payloads"
732
748
  );
733
749
  }
734
750
  const doc = new Y5.Doc();
735
751
  Y5.applyUpdate(doc, payload.data);
752
+ return createYjsReplica(doc);
753
+ },
754
+ parseVersion(serialized) {
755
+ return YjsVersion.parse(serialized);
756
+ }
757
+ };
758
+ var yjsSubstrateFactory = {
759
+ replica: yjsReplicaFactory,
760
+ createReplica() {
761
+ return createYjsReplica(new Y5.Doc());
762
+ },
763
+ upgrade(replica, schema) {
764
+ const doc = replica[BACKING_DOC];
765
+ ensureContainers(doc, schema, true);
736
766
  return createYjsSubstrate(doc, schema);
737
767
  },
768
+ create(schema) {
769
+ const doc = new Y5.Doc();
770
+ ensureContainers(doc, schema);
771
+ return createYjsSubstrate(doc, schema);
772
+ },
773
+ fromEntirety(payload, schema) {
774
+ const replica = this.createReplica();
775
+ replica.merge(payload);
776
+ return this.upgrade(replica, schema);
777
+ },
738
778
  parseVersion(serialized) {
739
779
  return YjsVersion.parse(serialized);
740
780
  }
@@ -746,7 +786,7 @@ function getSubstrate(doc) {
746
786
  const s = substrates.get(doc);
747
787
  if (!s) {
748
788
  throw new Error(
749
- "version/exportSnapshot/importDelta called on an object without a YjsSubstrate. Use a doc created by createYjsDoc() or createYjsDocFromSnapshot()."
789
+ "version/exportEntirety/merge called on an object without a YjsSubstrate. Use a doc created by createYjsDoc() or createYjsDocFromEntirety()."
750
790
  );
751
791
  }
752
792
  return s;
@@ -767,30 +807,27 @@ var createYjsDoc = (schema, doc) => {
767
807
  }
768
808
  return registerDoc(schema, yjsSubstrateFactory.create(schema));
769
809
  };
770
- var createYjsDocFromSnapshot = (schema, payload) => registerDoc(schema, yjsSubstrateFactory.fromSnapshot(payload, schema));
810
+ var createYjsDocFromEntirety = (schema, payload) => registerDoc(schema, yjsSubstrateFactory.fromEntirety(payload, schema));
771
811
 
772
812
  // src/sync.ts
773
813
  function version(doc) {
774
814
  return getSubstrate(doc).version();
775
815
  }
776
- function exportSnapshot(doc) {
777
- return getSubstrate(doc).exportSnapshot();
816
+ function exportEntirety(doc) {
817
+ return getSubstrate(doc).exportEntirety();
778
818
  }
779
819
  function exportSince(doc, since) {
780
820
  return getSubstrate(doc).exportSince(since);
781
821
  }
782
- function importDelta(doc, payload, origin) {
783
- getSubstrate(doc).importDelta(payload, origin);
822
+ function merge(doc, payload, origin) {
823
+ getSubstrate(doc).merge(payload, origin);
784
824
  }
785
825
 
786
826
  // src/index.ts
787
- import { applyChanges, change } from "@kyneta/schema";
788
- import { subscribe, subscribeNode } from "@kyneta/schema";
789
- import { Schema } from "@kyneta/schema";
790
827
  import { Schema as Schema2 } from "@kyneta/schema";
791
828
 
792
829
  // src/bind-yjs.ts
793
- import { bind } from "@kyneta/schema";
830
+ import { BACKING_DOC as BACKING_DOC2, bind, STRUCTURAL_YJS_CLIENT_ID as STRUCTURAL_YJS_CLIENT_ID2 } from "@kyneta/schema";
794
831
  import * as Y6 from "yjs";
795
832
  function hashPeerId(peerId) {
796
833
  let hash = 2166136261;
@@ -798,28 +835,33 @@ function hashPeerId(peerId) {
798
835
  hash ^= peerId.charCodeAt(i);
799
836
  hash = Math.imul(hash, 16777619);
800
837
  }
801
- return hash >>> 0;
838
+ const result = hash >>> 0;
839
+ return result === STRUCTURAL_YJS_CLIENT_ID2 ? 1 : result;
802
840
  }
803
841
  function createYjsFactory(peerId) {
804
842
  const numericClientId = hashPeerId(peerId);
805
843
  return {
806
- create(schema) {
807
- const doc = new Y6.Doc();
844
+ replica: yjsReplicaFactory,
845
+ createReplica() {
846
+ return createYjsReplica(new Y6.Doc());
847
+ },
848
+ upgrade(replica, schema) {
849
+ const doc = replica[BACKING_DOC2];
808
850
  doc.clientID = numericClientId;
809
- ensureContainers(doc, schema);
851
+ ensureContainers(doc, schema, true);
810
852
  return createYjsSubstrate(doc, schema);
811
853
  },
812
- fromSnapshot(payload, schema) {
813
- if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
814
- throw new Error(
815
- "YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads"
816
- );
817
- }
854
+ create(schema) {
818
855
  const doc = new Y6.Doc();
819
856
  doc.clientID = numericClientId;
820
- Y6.applyUpdate(doc, payload.data);
857
+ ensureContainers(doc, schema);
821
858
  return createYjsSubstrate(doc, schema);
822
859
  },
860
+ fromEntirety(payload, schema) {
861
+ const replica = this.createReplica();
862
+ replica.merge(payload);
863
+ return this.upgrade(replica, schema);
864
+ },
823
865
  parseVersion(serialized) {
824
866
  return YjsVersion.parse(serialized);
825
867
  }
@@ -833,6 +875,26 @@ function bindYjs(schema) {
833
875
  });
834
876
  }
835
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
+ );
894
+ }
895
+ return doc;
896
+ }
897
+
836
898
  // src/index.ts
837
899
  function text() {
838
900
  return Schema2.annotated("text");
@@ -845,13 +907,13 @@ export {
845
907
  bindYjs,
846
908
  change,
847
909
  createYjsDoc,
848
- createYjsDocFromSnapshot,
910
+ createYjsDocFromEntirety,
849
911
  createYjsSubstrate,
850
912
  ensureContainers,
851
913
  eventsToOps,
914
+ exportEntirety,
852
915
  exportSince,
853
- exportSnapshot,
854
- importDelta,
916
+ merge,
855
917
  resolveYjsType,
856
918
  stepIntoYjs,
857
919
  subscribe,
@@ -859,7 +921,7 @@ export {
859
921
  text,
860
922
  version,
861
923
  yjs,
862
- yjsStoreReader,
924
+ yjsReader,
863
925
  yjsSubstrateFactory
864
926
  };
865
927
  //# sourceMappingURL=index.js.map