@kyneta/yjs-schema 1.7.0 → 1.8.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 CHANGED
@@ -139,6 +139,8 @@ yjsDoc.getMap("root").toJSON() // raw state
139
139
  yjsDoc.clientID // client ID
140
140
  ```
141
141
 
142
+ *Note for raw Y.Doc consumers:* Subscribers attached directly to the underlying `Y.Doc` will newly see `options.origin` faithfully on `transaction.origin` (where previously it was silently dropped).
143
+
142
144
  ## Yjs Ecosystem Compatibility
143
145
 
144
146
  Because `yjs(doc)` returns a standard `Y.Doc`, the entire Yjs provider ecosystem works out of the box:
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/native-map.ts","../src/bind-yjs.ts","../src/change-mapping.ts","../src/populate.ts","../src/position.ts","../src/version.ts","../src/substrate.ts","../src/yjs-resolve.ts"],"mappings":";;;;;;;;;;AAwBA;;;;;;;;;;;;UAAiB,YAAA,SAAqB,SAAA;EAAA,SAC3B,IAAA,EAAM,CAAA,CAAE,GAAA;EAAA,SACR,IAAA,EAAM,CAAA,CAAE,IAAA;EAAA,SACR,OAAA;EAAA,SACA,IAAA,EAAM,CAAA,CAAE,KAAA;EAAA,SACR,WAAA;EAAA,SACA,MAAA,EAAQ,CAAA,CAAE,GAAA;EAAA,SACV,GAAA,EAAK,CAAA,CAAE,GAAA;EAAA,SACP,IAAA;EAAA,SACA,GAAA;EAAA,SACA,MAAA;EAAA,SACA,GAAA;AAAA;;;;;;;KC8GC,OAAA;;;;;;;;;;;;;;cAmBC,GAAA,EAAK,aAAA,CAAc,OAAA,EAAS,YAAA;;;;;;;AD5IzC;;;;;;;;iBEqCgB,gBAAA,CACd,OAAA,EAAS,CAAA,CAAE,GAAA,OACX,UAAA,EAAY,QAAA,EACZ,IAAA,EAAM,IAAA,EACN,MAAA,EAAQ,UAAA,EACR,OAAA,GAAU,aAAA;;;;;;;;;;;;;;iBAscI,WAAA,CACd,MAAA,EAAQ,CAAA,CAAE,MAAA,SACV,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA,GACT,IAAA;;;;;;;AFpfH;;;;;;;;;;;;;;;;;;;;;;;iBG6BgB,gBAAA,CACd,GAAA,EAAK,CAAA,CAAE,GAAA,EACP,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA;;;;iBC9CI,UAAA,CAAW,IAAU,EAAJ,IAAI;;iBAKrB,YAAA,CAAa,KAAA,WAAgB,IAAI;AAAA,cAIpC,WAAA,YAAuB,QAAA;EAAA,iBAIf,IAAA;EAAA,iBACA,GAAA;EAAA,SAJV,IAAA,EAAM,IAAA;cAGI,IAAA,EAAM,CAAA,CAAE,gBAAA,EACR,GAAA,EAAK,CAAA,CAAE,GAAA;EAK1B,OAAA,CAAA;EAQA,MAAA,CAAA,GAAU,UAAA;EAIV,SAAA,CAAU,aAAA,WAAwB,WAAA;AAAA;;;;;;;AJjBpC;;;;;;;;;;;;;;;cK8Ea,UAAA,YAAsB,OAAA;EL5ElB;EAAA,SK8EN,EAAA,EAAI,UAAA;EL7EJ;;;;;EAAA,SKoFA,aAAA,EAAe,UAAA;cAEZ,EAAA,EAAI,UAAA,EAAY,aAAA,GAAgB,UAAA;ELnFzB;;;;;;EAAA,OK+FZ,OAAA,CAAQ,GAAA,EAAK,GAAA,GAAM,UAAA;EL1FjB;;AAAG;;;;AC8Gd;;;ED9GW,OKyGF,aAAA,CACL,GAAA,EAAK,GAAA,EACL,EAAA,EAAI,UAAA,QADI,CAAA,CAC4B,eAAA,IACnC,UAAA;EJEc;AAmBnB;;;;;EIRE,SAAA,CAAA;EJQ6B;;;;;;AAAsB;;;;ACvGrD;;;;EGmHE,OAAA,CAAQ,KAAA,EAAO,OAAA;EHhHT;;;;;;;;;;;;;EG4IN,IAAA,CAAK,KAAA,EAAO,OAAA,GAAU,UAAA;EH1IZ;;;AAAa;AAsczB;;;;;EAtcY,OG+JH,KAAA,CAAM,UAAA,WAAqB,UAAA;AAAA;;;;;;ALzMpC;;;;;;;;;;;;;;;;iBMiEgB,kBAAA,CACd,GAAA,EAAK,CAAA,CAAE,GAAA,EACP,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA,GACT,SAAA,CAAU,UAAA;AAAA,cAkWA,iBAAA,EAAmB,cAAc,CAAC,UAAA;AAAA,cA8BlC,mBAAA,EAAqB,gBAAgB,CAAC,UAAA;;;;;;;ANrcnD;;;;;;;;;cOiBa,WAAA,EAAa,WAsBzB;;;;;;;;;;;;iBAiBe,cAAA,CACd,OAAA,EAAS,CAAA,CAAE,GAAA,OACX,UAAA,EAAY,QAAA,EACZ,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,aAAA,GACT,cAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/native-map.ts","../src/bind-yjs.ts","../src/change-mapping.ts","../src/populate.ts","../src/position.ts","../src/version.ts","../src/substrate.ts","../src/yjs-resolve.ts"],"mappings":";;;;;;;;;;AAwBA;;;;;;;;;;;;UAAiB,YAAA,SAAqB,SAAA;EAAA,SAC3B,IAAA,EAAM,CAAA,CAAE,GAAA;EAAA,SACR,IAAA,EAAM,CAAA,CAAE,IAAA;EAAA,SACR,OAAA;EAAA,SACA,IAAA,EAAM,CAAA,CAAE,KAAA;EAAA,SACR,WAAA;EAAA,SACA,MAAA,EAAQ,CAAA,CAAE,GAAA;EAAA,SACV,GAAA,EAAK,CAAA,CAAE,GAAA;EAAA,SACP,IAAA;EAAA,SACA,GAAA;EAAA,SACA,MAAA;EAAA,SACA,GAAA;AAAA;;;;;;;KC8GC,OAAA;;;;;;;;;;;;;;cAmBC,GAAA,EAAK,aAAA,CAAc,OAAA,EAAS,YAAA;;;;;;;AD5IzC;;;;;;;;iBEuCgB,gBAAA,CACd,OAAA,EAAS,CAAA,CAAE,GAAA,OACX,UAAA,EAAY,QAAA,EACZ,IAAA,EAAM,IAAA,EACN,MAAA,EAAQ,UAAA,EACR,OAAA,GAAU,aAAA;;;;;;;;;;;;;;iBAkcI,WAAA,CACd,MAAA,EAAQ,CAAA,CAAE,MAAA,SACV,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA,GACT,IAAA;;;;;;;AFlfH;;;;;;;;;;;;;;;;;;;;;;;iBG6BgB,gBAAA,CACd,GAAA,EAAK,CAAA,CAAE,GAAA,EACP,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA;;;;iBC9CI,UAAA,CAAW,IAAU,EAAJ,IAAI;;iBAKrB,YAAA,CAAa,KAAA,WAAgB,IAAI;AAAA,cAIpC,WAAA,YAAuB,QAAA;EAAA,iBAIf,IAAA;EAAA,iBACA,GAAA;EAAA,SAJV,IAAA,EAAM,IAAA;cAGI,IAAA,EAAM,CAAA,CAAE,gBAAA,EACR,GAAA,EAAK,CAAA,CAAE,GAAA;EAK1B,OAAA,CAAA;EAQA,MAAA,CAAA,GAAU,UAAA;EAIV,SAAA,CAAU,aAAA,WAAwB,WAAA;AAAA;;;;;;;AJjBpC;;;;;;;;;;;;;;;cK8Ea,UAAA,YAAsB,OAAA;EL5ElB;EAAA,SK8EN,EAAA,EAAI,UAAA;EL7EJ;;;;;EAAA,SKoFA,aAAA,EAAe,UAAA;cAEZ,EAAA,EAAI,UAAA,EAAY,aAAA,GAAgB,UAAA;ELnFzB;;;;;;EAAA,OK+FZ,OAAA,CAAQ,GAAA,EAAK,GAAA,GAAM,UAAA;EL1FjB;;AAAG;;;;AC8Gd;;;ED9GW,OKyGF,aAAA,CACL,GAAA,EAAK,GAAA,EACL,EAAA,EAAI,UAAA,QADI,CAAA,CAC4B,eAAA,IACnC,UAAA;EJEc;AAmBnB;;;;;EIRE,SAAA,CAAA;EJQ6B;;;;;;AAAsB;;;;ACrGrD;;;;EGiHE,OAAA,CAAQ,KAAA,EAAO,OAAA;EH9GT;;;;;;;;;;;;;EG0IN,IAAA,CAAK,KAAA,EAAO,OAAA,GAAU,UAAA;EHxIZ;;;AAAa;AAkczB;;;;;EAlcY,OG6JH,KAAA,CAAM,UAAA,WAAqB,UAAA;AAAA;;;;;;ALzMpC;;;;;;;;;;;;;;;;iBMiGgB,kBAAA,CACd,GAAA,EAAK,CAAA,CAAE,GAAA,EACP,MAAA,EAAQ,QAAA,EACR,OAAA,GAAU,aAAA,GACT,SAAA,CAAU,UAAA;AAAA,cA2dA,iBAAA,EAAmB,cAAc,CAAC,UAAA;AAAA,cA8BlC,mBAAA,EAAqB,gBAAgB,CAAC,UAAA;;;;;;;AN9lBnD;;;;;;;;;cOiBa,WAAA,EAAa,WAsBzB;;;;;;;;;;;;iBAiBe,cAAA,CACd,OAAA,EAAS,CAAA,CAAE,GAAA,OACX,UAAA,EAAY,QAAA,EACZ,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,aAAA,GACT,cAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BACKING_DOC, KIND, NATIVE, RawPath, STRUCTURAL_YJS_CLIENT_ID, SYNC_COLLABORATIVE, Schema, applyChange, applyChanges, base64ToUint8Array, buildWritableContext, change, createBindingTarget, createDoc, createMaterializeInterpreter, createRef, deriveSchemaBinding, executeBatch, expandMapOpsToLeaves, exportEntirety, exportSince, foldPath, interpret, isNonNullObject, materializeContextFromResolver, merge, pathSchema, plainReader, richTextChange, subscribe, subscribeNode, uint8ArrayToBase64, unwrap, version, versionVectorCompare, versionVectorMeet } from "@kyneta/schema";
1
+ import { BACKING_DOC, KIND, NATIVE, RECORD_INVERSE, RawPath, STRUCTURAL_YJS_CLIENT_ID, SYNC_COLLABORATIVE, Schema, applyChange, applyChanges, base64ToUint8Array, buildWritableContext, change, createBindingTarget, createDoc, createMaterializeInterpreter, createRef, deepClonePreState, deriveSchemaBinding, executeBatch, expandMapOpsToLeaves, exportEntirety, exportSince, findJsonBoundary, foldPath, interpret, invert, isJsonBoundary, isNonNullObject, isPlainObject, materializeContextFromResolver, merge, pathSchema, plainReader, richTextChange, subscribe, subscribeNode, syncShadow, uint8ArrayToBase64, unwrap, version, versionVectorCompare, versionVectorMeet } from "@kyneta/schema";
2
2
  import * as Y from "yjs";
3
3
  import { createSnapshot, decodeStateVector, encodeSnapshot, encodeStateVector, snapshot } from "yjs";
4
4
  //#region src/populate.ts
@@ -61,6 +61,7 @@ function ensureContainers(doc, schema, binding) {
61
61
  */
62
62
  function ensureRootField(rootMap, key, fieldSchema, binding, prefix) {
63
63
  if (rootMap.has(key)) return;
64
+ if (isJsonBoundary(fieldSchema)) return;
64
65
  switch (fieldSchema[KIND]) {
65
66
  case "text":
66
67
  case "richtext":
@@ -106,6 +107,7 @@ function ensureMapContainers(schema, binding, prefix) {
106
107
  for (const [key, fieldSchema] of Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b))) {
107
108
  const absPath = prefix ? `${prefix}.${key}` : key;
108
109
  const mapKey = binding?.forward.get(absPath) ?? key;
110
+ if (isJsonBoundary(fieldSchema)) continue;
109
111
  switch (fieldSchema[KIND]) {
110
112
  case "text":
111
113
  case "richtext":
@@ -258,7 +260,7 @@ function applyMapChange(rootMap, rootSchema, path, change, binding) {
258
260
  }
259
261
  }
260
262
  function applyReplaceChange(rootMap, rootSchema, path, change, binding) {
261
- if (path.length === 0) throw new Error("applyChangeToYjs: ReplaceChange at root path is not supported");
263
+ if (path.length === 0) throw new Error("Cannot replace the root document struct in a CRDT backend. The root identity is fixed. Please mutate its properties individually (e.g., `doc.myField.set(value)` instead of `doc.set({ myField: value })`).");
262
264
  const lastSeg = path.segments.at(-1);
263
265
  if (!lastSeg) throw new Error("replaceChangeToDiff: empty path");
264
266
  const parentPath = path.slice(0, -1);
@@ -291,6 +293,7 @@ function applyReplaceChange(rootMap, rootSchema, path, change, binding) {
291
293
  */
292
294
  function maybeCreateSharedType(value, schema) {
293
295
  if (schema === void 0) return value;
296
+ if (isJsonBoundary(schema)) return value;
294
297
  switch (schema[KIND]) {
295
298
  case "text": {
296
299
  const text = new Y.Text();
@@ -311,7 +314,7 @@ function maybeCreateSharedType(value, schema) {
311
314
  return text;
312
315
  }
313
316
  case "product":
314
- if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
317
+ if (!isPlainObject(value)) return value;
315
318
  return createStructuredMap(value, schema);
316
319
  case "sequence": {
317
320
  if (!Array.isArray(value)) return value;
@@ -322,7 +325,7 @@ function maybeCreateSharedType(value, schema) {
322
325
  return arr;
323
326
  }
324
327
  case "map": {
325
- if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
328
+ if (!isPlainObject(value)) return value;
326
329
  const map = new Y.Map();
327
330
  const valueSchema = schema.item;
328
331
  for (const [k, v] of Object.entries(value)) map.set(k, maybeCreateSharedType(v, valueSchema));
@@ -820,7 +823,7 @@ var YjsVersion = class YjsVersion {
820
823
  };
821
824
  //#endregion
822
825
  //#region src/substrate.ts
823
- const KYNETA_ORIGIN = "kyneta-prepare";
826
+ const KYNETA_MARK = Symbol("kyneta:own-commit");
824
827
  /**
825
828
  * Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
826
829
  *
@@ -841,46 +844,105 @@ const KYNETA_ORIGIN = "kyneta-prepare";
841
844
  * @param binding - Optional SchemaBinding for identity-keyed containers.
842
845
  */
843
846
  function createYjsSubstrate(doc, schema, binding) {
844
- const pendingChanges = [];
847
+ const jsonBoundaryBuffer = /* @__PURE__ */ new Map();
845
848
  let pendingMergeOrigin;
846
849
  let cachedCtx;
847
- let accumulatedDs = Y.createDeleteSetFromStructStore(doc.store);
848
850
  const rootMap = doc.getMap("root");
849
851
  const shadow = materializeYjsShadow(doc, schema, binding);
850
852
  const reader = plainReader(shadow);
853
+ /**
854
+ * Compute the identity-aware boundary key (or numeric index) for a
855
+ * json-boundary write at `prefixLength`. Mirrors the Loro substrate's
856
+ * `boundaryKey`; field segments inside a bound product get the
857
+ * identity hash, others pass through raw.
858
+ */
859
+ function boundaryKey(path, prefixLength) {
860
+ const seg = path.segments[prefixLength];
861
+ if (seg.role === "field" && binding) {
862
+ const absPath = path.segments.slice(0, prefixLength + 1).filter((s) => s.role === "field").map((s) => s.resolve()).join(".");
863
+ const identity = binding.forward.get(absPath);
864
+ if (identity) return identity;
865
+ }
866
+ return seg.resolve();
867
+ }
868
+ /**
869
+ * Buffer a json-boundary write. The boundary value is the entire σ
870
+ * subtree at the boundary path — already updated by the preceding
871
+ * `applyChange(shadow, ...)`. Subsequent writes inside the same
872
+ * subtree overwrite this entry (last-write-wins by σ snapshot).
873
+ *
874
+ * Returns silently when the parent container can't be resolved
875
+ * (root-level json fields land in `rootMap` directly — Yjs's
876
+ * root is the rootMap, so the parentResolved is `rootMap`).
877
+ */
878
+ function stageJsonBoundaryWrite(path, prefixLength) {
879
+ const { resolved: parent } = resolveYjsType(rootMap, schema, path.slice(0, prefixLength), binding);
880
+ const value = path.slice(0, prefixLength + 1).read(shadow);
881
+ const key = boundaryKey(path, prefixLength);
882
+ let target;
883
+ if (parent instanceof Y.Map) target = parent;
884
+ else if (parent instanceof Y.Array) target = parent;
885
+ else throw new Error(`yjs substrate: json-boundary write to unsupported parent type at path ${path.format()}`);
886
+ const slot = `${`${target._item?.id?.client ?? "root"}:${target._item?.id?.clock ?? "root"}`}/${String(key)}`;
887
+ jsonBoundaryBuffer.set(slot, {
888
+ target,
889
+ key,
890
+ value
891
+ });
892
+ }
893
+ /**
894
+ * Drain the json-boundary buffer into λ. Called from `afterBatch`
895
+ * inside the ambient `Y.transact` opened by `runBatch`. Each entry
896
+ * is applied as `target.set(key, value)` for Y.Map parents or as a
897
+ * delete+insert for Y.Array parents (Yjs Arrays don't have a
898
+ * `set(index, value)` primitive — replace = delete one + insert one).
899
+ */
900
+ function flushJsonBoundaryBuffer() {
901
+ if (jsonBoundaryBuffer.size === 0) return;
902
+ for (const { target, key, value } of jsonBoundaryBuffer.values()) if (target instanceof Y.Map) target.set(String(key), value);
903
+ else {
904
+ const index = key;
905
+ target.delete(index, 1);
906
+ target.insert(index, [value]);
907
+ }
908
+ jsonBoundaryBuffer.clear();
909
+ }
851
910
  const substrate = {
852
911
  [BACKING_DOC]: doc,
853
912
  reader,
854
913
  prepare(path, change, options) {
855
- if (!options?.replay) applyChange(shadow, path, change);
856
914
  if (options?.replay) return;
857
- pendingChanges.push({
858
- path,
859
- change
860
- });
915
+ const record = options?.[RECORD_INVERSE];
916
+ if (record && !options?.compensating) record(path, invert(deepClonePreState(path.read(shadow)), change));
917
+ applyChange(shadow, path, change);
918
+ const boundary = findJsonBoundary(schema, path, binding);
919
+ if (boundary !== null) {
920
+ stageJsonBoundaryWrite(path, boundary.prefixLength);
921
+ return;
922
+ }
923
+ applyChangeToYjs(rootMap, schema, path, change, binding);
861
924
  },
862
- onFlush(options) {
925
+ afterBatch(options) {
863
926
  if (options?.replay) {
864
- const fresh = materializeYjsShadow(doc, schema, binding);
865
- for (const key of Object.keys(fresh)) shadow[key] = fresh[key];
866
- for (const key of Object.keys(shadow)) if (!(key in fresh)) delete shadow[key];
927
+ syncShadow(shadow, materializeYjsShadow(doc, schema, binding));
867
928
  return;
868
929
  }
869
- if (pendingChanges.length === 0) return;
870
- doc.transact(() => {
871
- for (const { path, change } of pendingChanges) applyChangeToYjs(rootMap, schema, path, change, binding);
872
- }, KYNETA_ORIGIN);
873
- pendingChanges.length = 0;
930
+ flushJsonBoundaryBuffer();
931
+ },
932
+ runBatch(work, options) {
933
+ doc.transact((tr) => {
934
+ tr.meta.set(KYNETA_MARK, true);
935
+ work();
936
+ }, options?.origin);
874
937
  },
875
938
  context() {
876
- if (!cachedCtx) {
877
- cachedCtx = buildWritableContext(substrate);
878
- cachedCtx.nativeResolver = (nodeSchema, path) => {
939
+ if (!cachedCtx) cachedCtx = buildWritableContext(substrate, {
940
+ nativeResolver: (nodeSchema, path) => {
879
941
  if (path.segments.length === 0) return doc;
880
942
  if (nodeSchema[KIND] === "scalar" || nodeSchema[KIND] === "sum") return void 0;
881
943
  return resolveYjsType(rootMap, schema, path, binding).resolved;
882
- };
883
- cachedCtx.positionResolver = (_nodeSchema, path) => {
944
+ },
945
+ positionResolver: (_nodeSchema, path) => {
884
946
  return {
885
947
  createPosition(index, side) {
886
948
  const { resolved: ytype } = resolveYjsType(rootMap, schema, path, binding);
@@ -892,12 +954,12 @@ function createYjsSubstrate(doc, schema, binding) {
892
954
  return new YjsPosition(Y.decodeRelativePosition(bytes), doc);
893
955
  }
894
956
  };
895
- };
896
- }
957
+ }
958
+ });
897
959
  return cachedCtx;
898
960
  },
899
961
  version() {
900
- return YjsVersion.fromDeleteSet(doc, accumulatedDs);
962
+ return YjsVersion.fromDeleteSet(doc, Y.createDeleteSetFromStructStore(doc.store));
901
963
  },
902
964
  baseVersion() {
903
965
  return new YjsVersion(new Uint8Array([0]));
@@ -934,19 +996,15 @@ function createYjsSubstrate(doc, schema, binding) {
934
996
  }
935
997
  };
936
998
  rootMap.observeDeep((events, transaction) => {
937
- if (transaction.origin === KYNETA_ORIGIN) return;
999
+ if (transaction.meta.get(KYNETA_MARK)) return;
938
1000
  const ops = eventsToOps(events, schema, binding);
939
1001
  if (ops.length === 0) return;
940
- if (transaction.deleteSet.clients.size > 0) accumulatedDs = Y.mergeDeleteSets([accumulatedDs, transaction.deleteSet]);
941
1002
  const origin = pendingMergeOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
942
1003
  executeBatch(substrate.context(), ops, {
943
1004
  origin,
944
1005
  replay: true
945
1006
  });
946
1007
  });
947
- doc.on("afterTransaction", (transaction) => {
948
- if (transaction.origin === KYNETA_ORIGIN && transaction.deleteSet.clients.size > 0) accumulatedDs = Y.mergeDeleteSets([accumulatedDs, transaction.deleteSet]);
949
- });
950
1008
  return substrate;
951
1009
  }
952
1010
  /**