@kyneta/yjs-schema 1.6.1 → 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 +17 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +222 -207
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/__tests__/create.test.ts +11 -0
- package/src/__tests__/eager-write-coherence.test.ts +321 -0
- 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 +56 -3
- package/src/bind-yjs.ts +3 -5
- package/src/change-mapping.ts +73 -48
- package/src/index.ts +1 -2
- package/src/materialize.ts +109 -0
- package/src/populate.ts +35 -37
- package/src/substrate.ts +277 -111
- 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/src/populate.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// populate — Yjs container creation from schema structure.
|
|
2
2
|
//
|
|
3
3
|
// Ensures that the correct Yjs shared types (Y.Text, Y.Array, Y.Map)
|
|
4
|
-
// exist in a Y.Doc's root map to match the schema structure
|
|
5
|
-
// scalar/sum fields are initialized with Zero.structural defaults.
|
|
4
|
+
// exist in a Y.Doc's root map to match the schema structure.
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// without this, unset scalars would return undefined instead of their
|
|
11
|
-
// type-correct zero ("", 0, false).
|
|
6
|
+
// Only container types (text, product, sequence, map) require CRDT
|
|
7
|
+
// writes here. Scalar and sum fields are handled by the materializer's
|
|
8
|
+
// zero fallback — no Yjs writes are needed for non-container types.
|
|
12
9
|
//
|
|
13
10
|
// Root container strategy: All schema fields are children of a single
|
|
14
11
|
// root `Y.Map` obtained via `doc.getMap("root")`. This root map holds
|
|
@@ -20,7 +17,7 @@
|
|
|
20
17
|
// functions: ensureContainers, ensureRootField, ensureMapContainers.
|
|
21
18
|
|
|
22
19
|
import type { SchemaBinding, Schema as SchemaNode } from "@kyneta/schema"
|
|
23
|
-
import { KIND, STRUCTURAL_YJS_CLIENT_ID
|
|
20
|
+
import { isJsonBoundary, KIND, STRUCTURAL_YJS_CLIENT_ID } from "@kyneta/schema"
|
|
24
21
|
import * as Y from "yjs"
|
|
25
22
|
|
|
26
23
|
// ---------------------------------------------------------------------------
|
|
@@ -35,14 +32,10 @@ import * as Y from "yjs"
|
|
|
35
32
|
* schema's fields, and creates empty containers for each field within a
|
|
36
33
|
* single `doc.transact()` call for atomicity.
|
|
37
34
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* with stored operations).
|
|
43
|
-
*
|
|
44
|
-
* When `conditional` is false (default), all fields are created
|
|
45
|
-
* unconditionally. This is the correct mode for fresh documents.
|
|
35
|
+
* Container fields (text, product, sequence, map) are created if absent;
|
|
36
|
+
* existing containers are preserved (calling `rootMap.set()` on a field
|
|
37
|
+
* that already exists would be a destructive CRDT write). Scalar and sum
|
|
38
|
+
* fields are no-ops — the materializer handles zeros.
|
|
46
39
|
*
|
|
47
40
|
* **Structural identity:** This function temporarily sets `doc.clientID`
|
|
48
41
|
* to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
|
|
@@ -56,14 +49,11 @@ import * as Y from "yjs"
|
|
|
56
49
|
*
|
|
57
50
|
* @param doc - The Y.Doc to prepare
|
|
58
51
|
* @param schema - The root document schema (a ProductSchema)
|
|
59
|
-
* @param conditional - If true, skip fields that already exist in the root map.
|
|
60
|
-
* Context: jj:smmulzkm (two-phase substrate construction)
|
|
61
52
|
* @param binding - Optional SchemaBinding for identity-keyed containers.
|
|
62
53
|
*/
|
|
63
54
|
export function ensureContainers(
|
|
64
55
|
doc: Y.Doc,
|
|
65
56
|
schema: SchemaNode,
|
|
66
|
-
conditional = false,
|
|
67
57
|
binding?: SchemaBinding,
|
|
68
58
|
): void {
|
|
69
59
|
const rootMap = doc.getMap("root")
|
|
@@ -84,7 +74,6 @@ export function ensureContainers(
|
|
|
84
74
|
)) {
|
|
85
75
|
const identity = binding?.forward.get(key) as string | undefined
|
|
86
76
|
const mapKey = identity ?? key
|
|
87
|
-
if (conditional && rootMap.has(mapKey)) continue
|
|
88
77
|
ensureRootField(
|
|
89
78
|
rootMap,
|
|
90
79
|
mapKey,
|
|
@@ -112,7 +101,7 @@ export function ensureContainers(
|
|
|
112
101
|
* - `"product"` → empty Y.Map (recursive for nested products)
|
|
113
102
|
* - `"sequence"` → empty Y.Array
|
|
114
103
|
* - `"map"` → empty Y.Map
|
|
115
|
-
* - `"scalar"` / `"sum"` →
|
|
104
|
+
* - `"scalar"` / `"sum"` → no-op (materializer zero fallback)
|
|
116
105
|
* - `"counter"` / `"set"` / `"tree"` / `"movable"` → throw (not supported by Yjs)
|
|
117
106
|
*
|
|
118
107
|
* @param rootMap - The root Y.Map to set the field on.
|
|
@@ -128,6 +117,18 @@ function ensureRootField(
|
|
|
128
117
|
binding?: SchemaBinding,
|
|
129
118
|
prefix?: string,
|
|
130
119
|
): void {
|
|
120
|
+
// Skip fields that already exist — calling rootMap.set() on an existing
|
|
121
|
+
// shared type would replace it (a destructive CRDT write), and scalars
|
|
122
|
+
// are no-ops regardless. This is safe on fresh docs (nothing to skip)
|
|
123
|
+
// and necessary on hydrated docs (preserves existing data).
|
|
124
|
+
if (rootMap.has(key)) return
|
|
125
|
+
|
|
126
|
+
// JSON-boundary fields (struct.json/list.json/record.json) store the
|
|
127
|
+
// subtree as a plain JSON value in the root Y.Map entry. We leave the
|
|
128
|
+
// entry absent at structural-init time; the first write materialises
|
|
129
|
+
// it with `rootMap.set(key, plainValue)`.
|
|
130
|
+
if (isJsonBoundary(fieldSchema)) return
|
|
131
|
+
|
|
131
132
|
switch (fieldSchema[KIND]) {
|
|
132
133
|
case "text":
|
|
133
134
|
case "richtext":
|
|
@@ -147,16 +148,10 @@ function ensureRootField(
|
|
|
147
148
|
return
|
|
148
149
|
|
|
149
150
|
case "scalar":
|
|
150
|
-
case "sum":
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// type-correct values (e.g. "" not undefined for strings).
|
|
154
|
-
const zero = Zero.structural(fieldSchema)
|
|
155
|
-
if (zero !== undefined) {
|
|
156
|
-
rootMap.set(key, zero)
|
|
157
|
-
}
|
|
151
|
+
case "sum":
|
|
152
|
+
// Value concerns are handled by the materializer's zero fallback.
|
|
153
|
+
// No CRDT writes needed for non-container types.
|
|
158
154
|
return
|
|
159
|
-
}
|
|
160
155
|
|
|
161
156
|
case "counter":
|
|
162
157
|
case "set":
|
|
@@ -180,7 +175,7 @@ function ensureRootField(
|
|
|
180
175
|
*
|
|
181
176
|
* Only creates containers for fields that require Yjs shared types
|
|
182
177
|
* (text → Y.Text, product → Y.Map, sequence → Y.Array, map → Y.Map).
|
|
183
|
-
* Scalar and sum fields are
|
|
178
|
+
* Scalar and sum fields are skipped (materializer zero fallback).
|
|
184
179
|
*
|
|
185
180
|
* **Identity-keying:** When a `binding` is provided, computes the
|
|
186
181
|
* absolute schema path for each nested field (`prefix.fieldName`) and
|
|
@@ -207,6 +202,12 @@ function ensureMapContainers(
|
|
|
207
202
|
const identity = binding?.forward.get(absPath) as string | undefined
|
|
208
203
|
const mapKey = identity ?? key
|
|
209
204
|
|
|
205
|
+
// JSON-boundary nested field: leave the entry absent, the first
|
|
206
|
+
// write will set the plain JSON value at this key.
|
|
207
|
+
if (isJsonBoundary(fieldSchema)) {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
210
211
|
switch (fieldSchema[KIND]) {
|
|
211
212
|
case "text":
|
|
212
213
|
case "richtext":
|
|
@@ -226,13 +227,10 @@ function ensureMapContainers(
|
|
|
226
227
|
break
|
|
227
228
|
|
|
228
229
|
case "scalar":
|
|
229
|
-
case "sum":
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
map.set(mapKey, zero)
|
|
233
|
-
}
|
|
230
|
+
case "sum":
|
|
231
|
+
// Value concerns are handled by the materializer's zero fallback.
|
|
232
|
+
// No CRDT writes needed for non-container types.
|
|
234
233
|
break
|
|
235
|
-
}
|
|
236
234
|
|
|
237
235
|
case "counter":
|
|
238
236
|
case "set":
|