@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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -34
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/create.test.ts +11 -0
- package/src/__tests__/eager-write-coherence.test.ts +321 -0
- package/src/__tests__/substrate.test.ts +55 -0
- package/src/change-mapping.ts +11 -13
- package/src/populate.ts +13 -1
- package/src/substrate.ts +265 -112
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:
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;;;;;
|
|
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("
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
925
|
+
afterBatch(options) {
|
|
863
926
|
if (options?.replay) {
|
|
864
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
/**
|