@kyneta/yjs-schema 1.6.1 → 1.7.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 +17 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -178
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/__tests__/materialize.test.ts +227 -0
- package/src/__tests__/position.test.ts +7 -7
- package/src/__tests__/structural-merge.test.ts +18 -18
- package/src/__tests__/substrate.test.ts +1 -3
- package/src/bind-yjs.ts +3 -5
- package/src/change-mapping.ts +62 -35
- package/src/index.ts +1 -2
- package/src/materialize.ts +109 -0
- package/src/populate.ts +23 -37
- package/src/substrate.ts +27 -14
- package/src/yjs-extract.ts +52 -0
- package/src/yjs-resolve.ts +30 -95
- package/src/__tests__/reader.test.ts +0 -685
- package/src/reader.ts +0 -174
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BACKING_DOC, KIND, NATIVE, RawPath, STRUCTURAL_YJS_CLIENT_ID, SYNC_COLLABORATIVE, Schema,
|
|
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";
|
|
2
2
|
import * as Y from "yjs";
|
|
3
3
|
import { createSnapshot, decodeStateVector, encodeSnapshot, encodeStateVector, snapshot } from "yjs";
|
|
4
4
|
//#region src/populate.ts
|
|
@@ -10,14 +10,10 @@ import { createSnapshot, decodeStateVector, encodeSnapshot, encodeStateVector, s
|
|
|
10
10
|
* schema's fields, and creates empty containers for each field within a
|
|
11
11
|
* single `doc.transact()` call for atomicity.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* with stored operations).
|
|
18
|
-
*
|
|
19
|
-
* When `conditional` is false (default), all fields are created
|
|
20
|
-
* unconditionally. This is the correct mode for fresh documents.
|
|
13
|
+
* Container fields (text, product, sequence, map) are created if absent;
|
|
14
|
+
* existing containers are preserved (calling `rootMap.set()` on a field
|
|
15
|
+
* that already exists would be a destructive CRDT write). Scalar and sum
|
|
16
|
+
* fields are no-ops — the materializer handles zeros.
|
|
21
17
|
*
|
|
22
18
|
* **Structural identity:** This function temporarily sets `doc.clientID`
|
|
23
19
|
* to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
|
|
@@ -31,22 +27,16 @@ import { createSnapshot, decodeStateVector, encodeSnapshot, encodeStateVector, s
|
|
|
31
27
|
*
|
|
32
28
|
* @param doc - The Y.Doc to prepare
|
|
33
29
|
* @param schema - The root document schema (a ProductSchema)
|
|
34
|
-
* @param conditional - If true, skip fields that already exist in the root map.
|
|
35
|
-
* Context: jj:smmulzkm (two-phase substrate construction)
|
|
36
30
|
* @param binding - Optional SchemaBinding for identity-keyed containers.
|
|
37
31
|
*/
|
|
38
|
-
function ensureContainers(doc, schema,
|
|
32
|
+
function ensureContainers(doc, schema, binding) {
|
|
39
33
|
const rootMap = doc.getMap("root");
|
|
40
34
|
if (schema[KIND] !== "product") return;
|
|
41
35
|
const savedClientID = doc.clientID;
|
|
42
36
|
doc.clientID = STRUCTURAL_YJS_CLIENT_ID;
|
|
43
37
|
try {
|
|
44
38
|
doc.transact(() => {
|
|
45
|
-
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b)))
|
|
46
|
-
const mapKey = binding?.forward.get(key) ?? key;
|
|
47
|
-
if (conditional && rootMap.has(mapKey)) continue;
|
|
48
|
-
ensureRootField(rootMap, mapKey, fieldSchema, binding, key);
|
|
49
|
-
}
|
|
39
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b))) ensureRootField(rootMap, binding?.forward.get(key) ?? key, fieldSchema, binding, key);
|
|
50
40
|
});
|
|
51
41
|
} finally {
|
|
52
42
|
doc.clientID = savedClientID;
|
|
@@ -60,7 +50,7 @@ function ensureContainers(doc, schema, conditional = false, binding) {
|
|
|
60
50
|
* - `"product"` → empty Y.Map (recursive for nested products)
|
|
61
51
|
* - `"sequence"` → empty Y.Array
|
|
62
52
|
* - `"map"` → empty Y.Map
|
|
63
|
-
* - `"scalar"` / `"sum"` →
|
|
53
|
+
* - `"scalar"` / `"sum"` → no-op (materializer zero fallback)
|
|
64
54
|
* - `"counter"` / `"set"` / `"tree"` / `"movable"` → throw (not supported by Yjs)
|
|
65
55
|
*
|
|
66
56
|
* @param rootMap - The root Y.Map to set the field on.
|
|
@@ -70,6 +60,7 @@ function ensureContainers(doc, schema, conditional = false, binding) {
|
|
|
70
60
|
* @param prefix - The absolute schema path prefix for this field (used for nested lookups).
|
|
71
61
|
*/
|
|
72
62
|
function ensureRootField(rootMap, key, fieldSchema, binding, prefix) {
|
|
63
|
+
if (rootMap.has(key)) return;
|
|
73
64
|
switch (fieldSchema[KIND]) {
|
|
74
65
|
case "text":
|
|
75
66
|
case "richtext":
|
|
@@ -85,11 +76,7 @@ function ensureRootField(rootMap, key, fieldSchema, binding, prefix) {
|
|
|
85
76
|
rootMap.set(key, new Y.Map());
|
|
86
77
|
return;
|
|
87
78
|
case "scalar":
|
|
88
|
-
case "sum":
|
|
89
|
-
const zero = Zero.structural(fieldSchema);
|
|
90
|
-
if (zero !== void 0) rootMap.set(key, zero);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
79
|
+
case "sum": return;
|
|
93
80
|
case "counter":
|
|
94
81
|
case "set":
|
|
95
82
|
case "tree":
|
|
@@ -102,7 +89,7 @@ function ensureRootField(rootMap, key, fieldSchema, binding, prefix) {
|
|
|
102
89
|
*
|
|
103
90
|
* Only creates containers for fields that require Yjs shared types
|
|
104
91
|
* (text → Y.Text, product → Y.Map, sequence → Y.Array, map → Y.Map).
|
|
105
|
-
* Scalar and sum fields are
|
|
92
|
+
* Scalar and sum fields are skipped (materializer zero fallback).
|
|
106
93
|
*
|
|
107
94
|
* **Identity-keying:** When a `binding` is provided, computes the
|
|
108
95
|
* absolute schema path for each nested field (`prefix.fieldName`) and
|
|
@@ -134,11 +121,7 @@ function ensureMapContainers(schema, binding, prefix) {
|
|
|
134
121
|
map.set(mapKey, new Y.Map());
|
|
135
122
|
break;
|
|
136
123
|
case "scalar":
|
|
137
|
-
case "sum":
|
|
138
|
-
const zero = Zero.structural(fieldSchema);
|
|
139
|
-
if (zero !== void 0) map.set(mapKey, zero);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
124
|
+
case "sum": break;
|
|
142
125
|
case "counter":
|
|
143
126
|
case "set":
|
|
144
127
|
case "tree":
|
|
@@ -158,68 +141,29 @@ function ensureMapContainers(schema, binding, prefix) {
|
|
|
158
141
|
* - `Y.Text` → terminal (cannot step further)
|
|
159
142
|
* - Plain value → terminal (return `undefined`)
|
|
160
143
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
144
|
+
* `_nextSchema` is part of the `PathStepper` contract for Loro's root
|
|
145
|
+
* dispatch but is unused here — Yjs's `instanceof` dispatch doesn't
|
|
146
|
+
* need to look ahead at the next schema kind.
|
|
164
147
|
*/
|
|
165
|
-
|
|
148
|
+
const stepIntoYjs = (current, _nextSchema, segment, identity) => {
|
|
166
149
|
const resolved = segment.resolve();
|
|
167
150
|
if (current instanceof Y.Map) return current.get(identity ?? resolved);
|
|
168
151
|
if (current instanceof Y.Array) return current.get(resolved);
|
|
169
152
|
if (current instanceof Y.Text) throw new Error(`yjs-resolve: cannot step into Y.Text`);
|
|
170
|
-
}
|
|
153
|
+
};
|
|
171
154
|
/**
|
|
172
155
|
* Resolve a Yjs shared type (or plain value) at the given path.
|
|
173
156
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* When a `binding` is provided, each step computes the absolute schema
|
|
178
|
-
* path and looks up the identity hash from `binding.forward`. This
|
|
179
|
-
* identity hash is used instead of the field name at every product-field
|
|
180
|
-
* boundary (root and nested).
|
|
157
|
+
* Thin wrapper around `foldPath(stepIntoYjs, ...)`. Returns the
|
|
158
|
+
* `PathFoldResult` shape from core — `{ resolved, schema }`.
|
|
181
159
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* root schema.
|
|
160
|
+
* When a `binding` is provided, every product-field boundary uses the
|
|
161
|
+
* identity hash from `binding.forward` instead of the field name.
|
|
185
162
|
*
|
|
186
|
-
*
|
|
187
|
-
* @param rootSchema - The root document schema
|
|
188
|
-
* @param path - The path to resolve
|
|
189
|
-
* @param binding - Optional SchemaBinding for identity-keyed navigation.
|
|
163
|
+
* For an empty path, returns the root map and root schema.
|
|
190
164
|
*/
|
|
191
165
|
function resolveYjsType(rootMap, rootSchema, path, binding) {
|
|
192
|
-
|
|
193
|
-
let schema = rootSchema;
|
|
194
|
-
let absPath = "";
|
|
195
|
-
for (let i = 0; i < path.length; i++) {
|
|
196
|
-
const seg = path.segments[i];
|
|
197
|
-
if (!seg) throw new Error(`Missing segment at index ${i}`);
|
|
198
|
-
const nextSchema = advanceSchema(schema, seg);
|
|
199
|
-
let identity;
|
|
200
|
-
if (binding && seg.role === "key") {
|
|
201
|
-
const segStr = seg.resolve();
|
|
202
|
-
absPath = absPath ? `${absPath}.${segStr}` : segStr;
|
|
203
|
-
identity = binding.forward.get(absPath);
|
|
204
|
-
}
|
|
205
|
-
current = stepIntoYjs(current, seg, identity);
|
|
206
|
-
schema = nextSchema;
|
|
207
|
-
if (schema[KIND] === "sum" && i + 1 < path.length) {
|
|
208
|
-
for (let j = i + 1; j < path.length; j++) {
|
|
209
|
-
const remaining = path.segments[j];
|
|
210
|
-
if (!remaining) throw new Error(`Missing segment at index ${j}`);
|
|
211
|
-
current = current?.[remaining.resolve()];
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
resolved: current,
|
|
215
|
-
schema
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
resolved: current,
|
|
221
|
-
schema
|
|
222
|
-
};
|
|
166
|
+
return foldPath(rootMap, rootSchema, path, stepIntoYjs, binding);
|
|
223
167
|
}
|
|
224
168
|
//#endregion
|
|
225
169
|
//#region src/change-mapping.ts
|
|
@@ -254,6 +198,7 @@ function applyChangeToYjs(rootMap, rootSchema, path, change, binding) {
|
|
|
254
198
|
return;
|
|
255
199
|
case "increment": throw new Error(`Yjs substrate does not support "${change.type}" changes. Counter requires a CRDT backend that supports counters (e.g. Loro). Attempted IncrementChange with amount=${change.amount} at path [${pathToString(path)}].`);
|
|
256
200
|
case "tree": throw new Error(`Yjs substrate does not support "${change.type}" changes. Tree requires a CRDT backend that supports trees (e.g. Loro). Attempted TreeChange at path [${pathToString(path)}].`);
|
|
201
|
+
case "set-op": throw new Error(`Yjs substrate does not support "${change.type}" changes. Schema.set requires "add-wins-per-key" which is not in YjsLaws. Attempted SetChange at path [${pathToString(path)}].`);
|
|
257
202
|
default: throw new Error(`applyChangeToYjs: unsupported change type "${change.type}"`);
|
|
258
203
|
}
|
|
259
204
|
}
|
|
@@ -284,7 +229,7 @@ function applyRichTextChange(rootMap, rootSchema, path, change, binding) {
|
|
|
284
229
|
function applySequenceChange(rootMap, rootSchema, path, change, binding) {
|
|
285
230
|
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
286
231
|
if (!(resolved instanceof Y.Array)) throw new Error(`applyChangeToYjs: SequenceChange target at path [${pathToString(path)}] is not a Y.Array`);
|
|
287
|
-
const itemSchema = getItemSchema(
|
|
232
|
+
const itemSchema = getItemSchema(pathSchema(rootSchema, path));
|
|
288
233
|
let cursor = 0;
|
|
289
234
|
for (const instruction of change.instructions) if ("retain" in instruction) cursor += instruction.retain;
|
|
290
235
|
else if ("delete" in instruction) resolved.delete(cursor, instruction.delete);
|
|
@@ -298,13 +243,13 @@ function applySequenceChange(rootMap, rootSchema, path, change, binding) {
|
|
|
298
243
|
function applyMapChange(rootMap, rootSchema, path, change, binding) {
|
|
299
244
|
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
300
245
|
if (!(resolved instanceof Y.Map)) throw new Error(`applyChangeToYjs: MapChange target at path [${pathToString(path)}] is not a Y.Map`);
|
|
301
|
-
const targetSchema =
|
|
246
|
+
const targetSchema = pathSchema(rootSchema, path);
|
|
302
247
|
if (change.delete) for (const key of change.delete) resolved.delete(key);
|
|
303
248
|
if (change.set) for (const [key, value] of Object.entries(change.set)) {
|
|
304
249
|
const yjsValue = maybeCreateSharedType(value, getFieldSchema(targetSchema, key));
|
|
305
250
|
let mapKey = key;
|
|
306
251
|
if (binding && targetSchema[KIND] === "product") {
|
|
307
|
-
const parentAbsPath = path.segments.filter((s) => s.role === "
|
|
252
|
+
const parentAbsPath = path.segments.filter((s) => s.role === "field").map((s) => s.resolve()).join(".");
|
|
308
253
|
const absPath = parentAbsPath ? `${parentAbsPath}.${key}` : key;
|
|
309
254
|
const identity = binding.forward.get(absPath);
|
|
310
255
|
if (identity) mapKey = identity;
|
|
@@ -319,18 +264,18 @@ function applyReplaceChange(rootMap, rootSchema, path, change, binding) {
|
|
|
319
264
|
const parentPath = path.slice(0, -1);
|
|
320
265
|
const { resolved: parent } = resolveYjsType(rootMap, rootSchema, parentPath, binding);
|
|
321
266
|
const resolved = lastSeg.resolve();
|
|
322
|
-
if (parent instanceof Y.Map && lastSeg.role === "
|
|
323
|
-
const targetSchema =
|
|
267
|
+
if (parent instanceof Y.Map && (lastSeg.role === "field" || lastSeg.role === "entry")) {
|
|
268
|
+
const targetSchema = pathSchema(rootSchema, path);
|
|
324
269
|
const yjsValue = maybeCreateSharedType(change.value, targetSchema);
|
|
325
270
|
let mapKey = resolved;
|
|
326
|
-
if (binding) {
|
|
327
|
-
const absPath = path.segments.filter((s) => s.role === "
|
|
271
|
+
if (binding && lastSeg.role === "field") {
|
|
272
|
+
const absPath = path.segments.filter((s) => s.role === "field").map((s) => s.resolve()).join(".");
|
|
328
273
|
const identity = binding.forward.get(absPath);
|
|
329
274
|
if (identity) mapKey = identity;
|
|
330
275
|
}
|
|
331
276
|
parent.set(mapKey, yjsValue);
|
|
332
277
|
} else if (parent instanceof Y.Array && lastSeg.role === "index") {
|
|
333
|
-
const targetSchema =
|
|
278
|
+
const targetSchema = pathSchema(rootSchema, path);
|
|
334
279
|
const yjsValue = maybeCreateSharedType(change.value, targetSchema);
|
|
335
280
|
parent.delete(resolved, 1);
|
|
336
281
|
parent.insert(resolved, [yjsValue]);
|
|
@@ -431,7 +376,7 @@ function createStructuredMap(obj, productSchema) {
|
|
|
431
376
|
function eventsToOps(events, schema, binding) {
|
|
432
377
|
const ops = [];
|
|
433
378
|
for (const event of events) {
|
|
434
|
-
const kynetaPath = yjsPathToKynetaPath(event.path, binding);
|
|
379
|
+
const kynetaPath = yjsPathToKynetaPath(event.path, schema, binding);
|
|
435
380
|
const change = eventToChange(event, schema, kynetaPath, binding);
|
|
436
381
|
if (change) ops.push({
|
|
437
382
|
path: kynetaPath,
|
|
@@ -441,21 +386,44 @@ function eventsToOps(events, schema, binding) {
|
|
|
441
386
|
return expandMapOpsToLeaves(ops, schema);
|
|
442
387
|
}
|
|
443
388
|
/**
|
|
444
|
-
* Convert a Yjs event path
|
|
389
|
+
* Convert a Yjs event path to a kyneta `RawPath`, walking the schema
|
|
390
|
+
* alongside so each segment is classified as field / entry / index by
|
|
391
|
+
* the current schema kind.
|
|
445
392
|
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
393
|
+
* Why schema-aware and not "did the inverse lookup hit?": the binding's
|
|
394
|
+
* inverse map only covers declared product-field positions reachable
|
|
395
|
+
* without crossing a runtime-keyed container. A declared struct field
|
|
396
|
+
* nested under a `record(...)` value type is reachable via Yjs but
|
|
397
|
+
* absent from `binding.inverse` — without the schema walk it would be
|
|
398
|
+
* misclassified as an entry and then rejected by `advanceSchema`.
|
|
448
399
|
*/
|
|
449
|
-
function yjsPathToKynetaPath(yjsPath, binding) {
|
|
400
|
+
function yjsPathToKynetaPath(yjsPath, rootSchema, binding) {
|
|
450
401
|
let path = RawPath.empty;
|
|
402
|
+
let schema = rootSchema;
|
|
451
403
|
for (const segment of yjsPath) if (typeof segment === "string") {
|
|
404
|
+
let leaf = segment;
|
|
452
405
|
const absPath = binding?.inverse.get(segment);
|
|
453
406
|
if (absPath) {
|
|
454
407
|
const lastDot = absPath.lastIndexOf(".");
|
|
455
|
-
|
|
408
|
+
leaf = lastDot >= 0 ? absPath.slice(lastDot + 1) : absPath;
|
|
409
|
+
}
|
|
410
|
+
const kind = schema?.[KIND];
|
|
411
|
+
if (kind === "product") {
|
|
456
412
|
path = path.field(leaf);
|
|
457
|
-
|
|
458
|
-
|
|
413
|
+
schema = schema?.fields[leaf];
|
|
414
|
+
} else if (kind === "map" || kind === "set" || kind === "tree") {
|
|
415
|
+
path = path.entry(leaf);
|
|
416
|
+
schema = schema?.item;
|
|
417
|
+
} else {
|
|
418
|
+
path = path.entry(leaf);
|
|
419
|
+
schema = void 0;
|
|
420
|
+
}
|
|
421
|
+
} else if (typeof segment === "number") {
|
|
422
|
+
path = path.item(segment);
|
|
423
|
+
const kind = schema?.[KIND];
|
|
424
|
+
if (kind === "sequence" || kind === "movable") schema = schema.item;
|
|
425
|
+
else schema = void 0;
|
|
426
|
+
}
|
|
459
427
|
return path;
|
|
460
428
|
}
|
|
461
429
|
/**
|
|
@@ -470,7 +438,7 @@ function yjsPathToKynetaPath(yjsPath, binding) {
|
|
|
470
438
|
*/
|
|
471
439
|
function eventToChange(event, rootSchema, kynetaPath, binding) {
|
|
472
440
|
if (event.target instanceof Y.Text) {
|
|
473
|
-
if (
|
|
441
|
+
if (pathSchema(rootSchema, kynetaPath)[KIND] === "richtext") return richTextEventToChange(event);
|
|
474
442
|
return textEventToChange(event);
|
|
475
443
|
}
|
|
476
444
|
if (event.target instanceof Y.Array) return arrayEventToChange(event);
|
|
@@ -583,17 +551,6 @@ function extractEventValue(value) {
|
|
|
583
551
|
return value;
|
|
584
552
|
}
|
|
585
553
|
/**
|
|
586
|
-
* Resolve the schema at a given path by walking through advanceSchema.
|
|
587
|
-
*/
|
|
588
|
-
function resolveSchemaAtPath(rootSchema, path) {
|
|
589
|
-
let schema = rootSchema;
|
|
590
|
-
for (const seg of path.segments) {
|
|
591
|
-
schema = advanceSchema(schema, seg);
|
|
592
|
-
if (schema[KIND] === "sum") return schema;
|
|
593
|
-
}
|
|
594
|
-
return schema;
|
|
595
|
-
}
|
|
596
|
-
/**
|
|
597
554
|
* Get the item schema from a sequence schema, if available.
|
|
598
555
|
*/
|
|
599
556
|
function getItemSchema(schema) {
|
|
@@ -612,35 +569,7 @@ function pathToString(path) {
|
|
|
612
569
|
return path.segments.map((seg) => String(seg.resolve())).join(".");
|
|
613
570
|
}
|
|
614
571
|
//#endregion
|
|
615
|
-
//#region src/
|
|
616
|
-
/** Map kyneta Side to Yjs assoc. Left → -1 (left-sticky), Right → 0 (right-sticky). */
|
|
617
|
-
function toYjsAssoc(side) {
|
|
618
|
-
return side === "left" ? -1 : 0;
|
|
619
|
-
}
|
|
620
|
-
/** Map Yjs assoc to kyneta Side. Negative → left, non-negative → right. */
|
|
621
|
-
function fromYjsAssoc(assoc) {
|
|
622
|
-
return assoc < 0 ? "left" : "right";
|
|
623
|
-
}
|
|
624
|
-
var YjsPosition = class {
|
|
625
|
-
rpos;
|
|
626
|
-
doc;
|
|
627
|
-
side;
|
|
628
|
-
constructor(rpos, doc) {
|
|
629
|
-
this.rpos = rpos;
|
|
630
|
-
this.doc = doc;
|
|
631
|
-
this.side = fromYjsAssoc(rpos.assoc);
|
|
632
|
-
}
|
|
633
|
-
resolve() {
|
|
634
|
-
const abs = Y.createAbsolutePositionFromRelativePosition(this.rpos, this.doc);
|
|
635
|
-
return abs ? abs.index : null;
|
|
636
|
-
}
|
|
637
|
-
encode() {
|
|
638
|
-
return Y.encodeRelativePosition(this.rpos);
|
|
639
|
-
}
|
|
640
|
-
transform(_instructions) {}
|
|
641
|
-
};
|
|
642
|
-
//#endregion
|
|
643
|
-
//#region src/reader.ts
|
|
572
|
+
//#region src/yjs-extract.ts
|
|
644
573
|
/**
|
|
645
574
|
* Extract a plain value from a Yjs shared type or return a plain value as-is.
|
|
646
575
|
*
|
|
@@ -674,50 +603,71 @@ function yTextToRichTextDelta(ytext) {
|
|
|
674
603
|
}
|
|
675
604
|
return spans;
|
|
676
605
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
* path segment.
|
|
681
|
-
*
|
|
682
|
-
* The reader is a live view — mutations to the underlying Y.Doc
|
|
683
|
-
* (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
|
|
684
|
-
* visible through the reader.
|
|
685
|
-
*
|
|
686
|
-
* Internally obtains the root map via `doc.getMap("root")`.
|
|
687
|
-
*
|
|
688
|
-
* @param doc - The Y.Doc to read from.
|
|
689
|
-
* @param schema - The root schema for the document.
|
|
690
|
-
* @param binding - Optional SchemaBinding for identity-keyed navigation.
|
|
691
|
-
*/
|
|
692
|
-
function yjsReader(doc, schema, binding) {
|
|
693
|
-
const rootMap = doc.getMap("root");
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/materialize.ts
|
|
608
|
+
function createYjsResolver(rootMap, rootSchema, binding) {
|
|
694
609
|
return {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
610
|
+
resolveValue(path) {
|
|
611
|
+
return extractValue(resolveYjsType(rootMap, rootSchema, path, binding).resolved);
|
|
612
|
+
},
|
|
613
|
+
resolveText(path) {
|
|
614
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
615
|
+
if (resolved instanceof Y.Text) return resolved.toJSON();
|
|
616
|
+
const value = extractValue(resolved);
|
|
617
|
+
return typeof value === "string" ? value : void 0;
|
|
700
618
|
},
|
|
701
|
-
|
|
702
|
-
|
|
619
|
+
resolveCounter(_path) {},
|
|
620
|
+
resolveRichText(path) {
|
|
621
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
622
|
+
if (resolved instanceof Y.Text) return yTextToRichTextDelta(resolved);
|
|
623
|
+
},
|
|
624
|
+
resolveLength(path) {
|
|
625
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
703
626
|
if (resolved instanceof Y.Array) return resolved.length;
|
|
704
|
-
|
|
705
|
-
return 0;
|
|
627
|
+
return Array.isArray(resolved) ? resolved.length : 0;
|
|
706
628
|
},
|
|
707
|
-
|
|
708
|
-
const { resolved } = resolveYjsType(rootMap,
|
|
629
|
+
resolveKeys(path) {
|
|
630
|
+
const { resolved } = resolveYjsType(rootMap, rootSchema, path, binding);
|
|
709
631
|
if (resolved instanceof Y.Map) return Array.from(resolved.keys());
|
|
710
|
-
|
|
711
|
-
return [];
|
|
632
|
+
return isNonNullObject(resolved) ? Object.keys(resolved) : [];
|
|
712
633
|
},
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (resolved instanceof Y.Map) return resolved.has(key);
|
|
716
|
-
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) return key in resolved;
|
|
717
|
-
return false;
|
|
634
|
+
resolveForest(_path) {
|
|
635
|
+
return [];
|
|
718
636
|
}
|
|
719
637
|
};
|
|
720
638
|
}
|
|
639
|
+
function materializeYjsShadow(doc, schema, binding) {
|
|
640
|
+
const resolver = createYjsResolver(doc.getMap("root"), schema, binding);
|
|
641
|
+
return interpret(schema, createMaterializeInterpreter(resolver), materializeContextFromResolver(resolver));
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/position.ts
|
|
645
|
+
/** Map kyneta Side to Yjs assoc. Left → -1 (left-sticky), Right → 0 (right-sticky). */
|
|
646
|
+
function toYjsAssoc(side) {
|
|
647
|
+
return side === "left" ? -1 : 0;
|
|
648
|
+
}
|
|
649
|
+
/** Map Yjs assoc to kyneta Side. Negative → left, non-negative → right. */
|
|
650
|
+
function fromYjsAssoc(assoc) {
|
|
651
|
+
return assoc < 0 ? "left" : "right";
|
|
652
|
+
}
|
|
653
|
+
var YjsPosition = class {
|
|
654
|
+
rpos;
|
|
655
|
+
doc;
|
|
656
|
+
side;
|
|
657
|
+
constructor(rpos, doc) {
|
|
658
|
+
this.rpos = rpos;
|
|
659
|
+
this.doc = doc;
|
|
660
|
+
this.side = fromYjsAssoc(rpos.assoc);
|
|
661
|
+
}
|
|
662
|
+
resolve() {
|
|
663
|
+
const abs = Y.createAbsolutePositionFromRelativePosition(this.rpos, this.doc);
|
|
664
|
+
return abs ? abs.index : null;
|
|
665
|
+
}
|
|
666
|
+
encode() {
|
|
667
|
+
return Y.encodeRelativePosition(this.rpos);
|
|
668
|
+
}
|
|
669
|
+
transform(_instructions) {}
|
|
670
|
+
};
|
|
721
671
|
//#endregion
|
|
722
672
|
//#region src/version.ts
|
|
723
673
|
/**
|
|
@@ -896,11 +846,13 @@ function createYjsSubstrate(doc, schema, binding) {
|
|
|
896
846
|
let cachedCtx;
|
|
897
847
|
let accumulatedDs = Y.createDeleteSetFromStructStore(doc.store);
|
|
898
848
|
const rootMap = doc.getMap("root");
|
|
899
|
-
const
|
|
849
|
+
const shadow = materializeYjsShadow(doc, schema, binding);
|
|
850
|
+
const reader = plainReader(shadow);
|
|
900
851
|
const substrate = {
|
|
901
852
|
[BACKING_DOC]: doc,
|
|
902
853
|
reader,
|
|
903
854
|
prepare(path, change, options) {
|
|
855
|
+
if (!options?.replay) applyChange(shadow, path, change);
|
|
904
856
|
if (options?.replay) return;
|
|
905
857
|
pendingChanges.push({
|
|
906
858
|
path,
|
|
@@ -908,7 +860,12 @@ function createYjsSubstrate(doc, schema, binding) {
|
|
|
908
860
|
});
|
|
909
861
|
},
|
|
910
862
|
onFlush(options) {
|
|
911
|
-
if (options?.replay)
|
|
863
|
+
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];
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
912
869
|
if (pendingChanges.length === 0) return;
|
|
913
870
|
doc.transact(() => {
|
|
914
871
|
for (const { path, change } of pendingChanges) applyChangeToYjs(rootMap, schema, path, change, binding);
|
|
@@ -1103,13 +1060,13 @@ const yjsSubstrateFactory = {
|
|
|
1103
1060
|
upgrade(replica, schema) {
|
|
1104
1061
|
const doc = replica[BACKING_DOC];
|
|
1105
1062
|
const binding = trivialBinding(schema);
|
|
1106
|
-
ensureContainers(doc, schema,
|
|
1063
|
+
ensureContainers(doc, schema, binding);
|
|
1107
1064
|
return createYjsSubstrate(doc, schema, binding);
|
|
1108
1065
|
},
|
|
1109
1066
|
create(schema) {
|
|
1110
1067
|
const doc = new Y.Doc();
|
|
1111
1068
|
const binding = trivialBinding(schema);
|
|
1112
|
-
ensureContainers(doc, schema,
|
|
1069
|
+
ensureContainers(doc, schema, binding);
|
|
1113
1070
|
return createYjsSubstrate(doc, schema, binding);
|
|
1114
1071
|
},
|
|
1115
1072
|
fromEntirety(payload, schema) {
|
|
@@ -1156,13 +1113,13 @@ function createYjsFactory(peerId, binding) {
|
|
|
1156
1113
|
upgrade(replica, schema) {
|
|
1157
1114
|
const doc = replica[BACKING_DOC];
|
|
1158
1115
|
doc.clientID = numericClientId;
|
|
1159
|
-
ensureContainers(doc, schema,
|
|
1116
|
+
ensureContainers(doc, schema, binding);
|
|
1160
1117
|
return createYjsSubstrate(doc, schema, binding);
|
|
1161
1118
|
},
|
|
1162
1119
|
create(schema) {
|
|
1163
1120
|
const doc = new Y.Doc();
|
|
1164
1121
|
doc.clientID = numericClientId;
|
|
1165
|
-
ensureContainers(doc, schema,
|
|
1122
|
+
ensureContainers(doc, schema, binding);
|
|
1166
1123
|
return createYjsSubstrate(doc, schema, binding);
|
|
1167
1124
|
},
|
|
1168
1125
|
fromEntirety(payload, schema) {
|
|
@@ -1194,6 +1151,6 @@ const yjs = createBindingTarget({
|
|
|
1194
1151
|
syncProtocol: SYNC_COLLABORATIVE
|
|
1195
1152
|
});
|
|
1196
1153
|
//#endregion
|
|
1197
|
-
export { NATIVE, Schema, YjsPosition, YjsVersion, applyChangeToYjs, applyChanges, change, createDoc, createRef, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, fromYjsAssoc, merge, resolveYjsType, stepIntoYjs, subscribe, subscribeNode, toYjsAssoc, unwrap, version, yjs,
|
|
1154
|
+
export { NATIVE, Schema, YjsPosition, YjsVersion, applyChangeToYjs, applyChanges, change, createDoc, createRef, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, fromYjsAssoc, merge, resolveYjsType, stepIntoYjs, subscribe, subscribeNode, toYjsAssoc, unwrap, version, yjs, yjsReplicaFactory, yjsSubstrateFactory };
|
|
1198
1155
|
|
|
1199
1156
|
//# sourceMappingURL=index.js.map
|