@kyneta/yjs-schema 1.1.0 → 1.3.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.
@@ -1,24 +1,30 @@
1
- import { change, Schema, subscribe } from "@kyneta/schema"
2
- import { describe, expect, it, vi } from "vitest"
1
+ import {
2
+ change,
3
+ createDoc,
4
+ createRef,
5
+ Schema,
6
+ subscribe,
7
+ unwrap,
8
+ } from "@kyneta/schema"
9
+ import { describe, expect, it } from "vitest"
3
10
  import * as Y from "yjs"
4
- import { createYjsDoc, createYjsDocFromEntirety } from "../create.js"
11
+ import { yjs } from "../bind-yjs.js"
12
+ import { exportEntirety, exportSince, merge, version } from "../index.js"
5
13
  import { ensureContainers } from "../populate.js"
6
- import { yjsSubstrateFactory } from "../substrate.js"
7
- import { exportEntirety, exportSince, merge, version } from "../sync.js"
14
+ import { createYjsSubstrate } from "../substrate.js"
8
15
  import { YjsVersion } from "../version.js"
9
- import { yjs } from "../yjs-escape.js"
10
16
 
11
17
  // ===========================================================================
12
18
  // Schemas used across tests
13
19
  // ===========================================================================
14
20
 
15
- const SimpleSchema = Schema.doc({
16
- title: Schema.annotated("text"),
21
+ const SimpleSchema = Schema.struct({
22
+ title: Schema.text(),
17
23
  count: Schema.number(),
18
24
  items: Schema.list(Schema.string()),
19
25
  })
20
26
 
21
- const StructListSchema = Schema.doc({
27
+ const StructListSchema = Schema.struct({
22
28
  tasks: Schema.list(
23
29
  Schema.struct({
24
30
  name: Schema.string(),
@@ -27,8 +33,8 @@ const StructListSchema = Schema.doc({
27
33
  ),
28
34
  })
29
35
 
30
- const NestedSchema = Schema.doc({
31
- title: Schema.annotated("text"),
36
+ const NestedSchema = Schema.struct({
37
+ title: Schema.text(),
32
38
  meta: Schema.struct({
33
39
  author: Schema.string(),
34
40
  tags: Schema.list(Schema.string()),
@@ -36,18 +42,26 @@ const NestedSchema = Schema.doc({
36
42
  labels: Schema.record(Schema.string()),
37
43
  })
38
44
 
45
+ // ===========================================================================
46
+ // Helpers
47
+ // ===========================================================================
48
+
49
+ const boundSimple = yjs.bind(SimpleSchema)
50
+ const boundStructList = yjs.bind(StructListSchema)
51
+ const boundNested = yjs.bind(NestedSchema)
52
+
39
53
  // ===========================================================================
40
54
  // Tests
41
55
  // ===========================================================================
42
56
 
43
- describe("createYjsDoc", () => {
57
+ describe("createDoc", () => {
44
58
  // -------------------------------------------------------------------------
45
59
  // Default values
46
60
  // -------------------------------------------------------------------------
47
61
 
48
62
  describe("with defaults", () => {
49
63
  it("creates a doc with empty containers for shared types", () => {
50
- const doc = createYjsDoc(SimpleSchema)
64
+ const doc = createDoc(boundSimple)
51
65
  // Text annotation returns "" (empty Y.Text)
52
66
  expect(doc.title()).toBe("")
53
67
  // Plain scalars return structural zeros
@@ -57,7 +71,7 @@ describe("createYjsDoc", () => {
57
71
  })
58
72
 
59
73
  it("creates a doc with nested struct empty containers", () => {
60
- const doc = createYjsDoc(NestedSchema)
74
+ const doc = createDoc(boundNested)
61
75
  expect(doc.title()).toBe("")
62
76
  // Plain scalar inside struct returns structural zero
63
77
  expect(doc.meta.author()).toBe("")
@@ -66,7 +80,7 @@ describe("createYjsDoc", () => {
66
80
  })
67
81
 
68
82
  it("creates a doc with struct list defaults", () => {
69
- const doc = createYjsDoc(StructListSchema)
83
+ const doc = createDoc(boundStructList)
70
84
  expect(doc.tasks()).toEqual([])
71
85
  expect(doc.tasks.length).toBe(0)
72
86
  })
@@ -78,7 +92,7 @@ describe("createYjsDoc", () => {
78
92
 
79
93
  describe("with seeds", () => {
80
94
  it("creates a doc with scalar seed values", () => {
81
- const doc = createYjsDoc(SimpleSchema)
95
+ const doc = createDoc(boundSimple)
82
96
  change(doc, (d: any) => {
83
97
  d.title.insert(0, "Hello")
84
98
  d.count.set(42)
@@ -94,7 +108,7 @@ describe("createYjsDoc", () => {
94
108
  })
95
109
 
96
110
  it("creates a doc with partial seed (defaults fill gaps)", () => {
97
- const doc = createYjsDoc(SimpleSchema)
111
+ const doc = createDoc(boundSimple)
98
112
  change(doc, (d: any) => {
99
113
  d.title.insert(0, "Partial")
100
114
  })
@@ -104,7 +118,7 @@ describe("createYjsDoc", () => {
104
118
  })
105
119
 
106
120
  it("creates a doc with nested struct seed", () => {
107
- const doc = createYjsDoc(NestedSchema)
121
+ const doc = createDoc(boundNested)
108
122
  change(doc, (d: any) => {
109
123
  d.title.insert(0, "Doc")
110
124
  d.meta.author.set("Alice")
@@ -119,7 +133,7 @@ describe("createYjsDoc", () => {
119
133
  })
120
134
 
121
135
  it("creates a doc with struct list seed items", () => {
122
- const doc = createYjsDoc(StructListSchema)
136
+ const doc = createDoc(boundStructList)
123
137
  // Separate change() calls for list pushes to preserve order
124
138
  change(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
125
139
  change(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
@@ -146,7 +160,10 @@ describe("createYjsDoc", () => {
146
160
  ;(rootMap.get("items") as Y.Array<string>).push(["x"])
147
161
  })
148
162
 
149
- const doc = createYjsDoc(SimpleSchema, yjsDoc)
163
+ const doc = createRef(
164
+ SimpleSchema,
165
+ createYjsSubstrate(yjsDoc, SimpleSchema),
166
+ )
150
167
  expect(doc.title()).toBe("External")
151
168
  expect(doc.count()).toBe(99)
152
169
  expect(doc.items()).toEqual(["x"])
@@ -156,7 +173,10 @@ describe("createYjsDoc", () => {
156
173
  const yjsDoc = new Y.Doc()
157
174
  ensureContainers(yjsDoc, SimpleSchema)
158
175
 
159
- const doc = createYjsDoc(SimpleSchema, yjsDoc)
176
+ const doc = createRef(
177
+ SimpleSchema,
178
+ createYjsSubstrate(yjsDoc, SimpleSchema),
179
+ )
160
180
  change(doc, (d: any) => {
161
181
  d.title.insert(0, "Hello")
162
182
  d.count.set(42)
@@ -171,7 +191,10 @@ describe("createYjsDoc", () => {
171
191
  const yjsDoc = new Y.Doc()
172
192
  ensureContainers(yjsDoc, SimpleSchema)
173
193
 
174
- const doc = createYjsDoc(SimpleSchema, yjsDoc)
194
+ const doc = createRef(
195
+ SimpleSchema,
196
+ createYjsSubstrate(yjsDoc, SimpleSchema),
197
+ )
175
198
 
176
199
  const rootMap = yjsDoc.getMap("root")
177
200
  rootMap.set("count", 77)
@@ -179,12 +202,15 @@ describe("createYjsDoc", () => {
179
202
  expect(doc.count()).toBe(77)
180
203
  })
181
204
 
182
- it("yjs() escape hatch returns the same Y.Doc", () => {
205
+ it("unwrap() escape hatch returns the same Y.Doc", () => {
183
206
  const yjsDoc = new Y.Doc()
184
207
  ensureContainers(yjsDoc, SimpleSchema)
185
208
 
186
- const doc = createYjsDoc(SimpleSchema, yjsDoc)
187
- const escaped = yjs(doc)
209
+ const doc = createRef(
210
+ SimpleSchema,
211
+ createYjsSubstrate(yjsDoc, SimpleSchema),
212
+ )
213
+ const escaped = unwrap(doc)
188
214
 
189
215
  expect(escaped).toBe(yjsDoc)
190
216
  })
@@ -192,12 +218,12 @@ describe("createYjsDoc", () => {
192
218
  })
193
219
 
194
220
  // ===========================================================================
195
- // createYjsDocFromEntirety
221
+ // createDoc with payload (fromEntirety)
196
222
  // ===========================================================================
197
223
 
198
- describe("createYjsDocFromEntirety", () => {
224
+ describe("createDoc with payload", () => {
199
225
  it("reconstructs state from a snapshot", () => {
200
- const doc1 = createYjsDoc(SimpleSchema)
226
+ const doc1 = createDoc(boundSimple)
201
227
  change(doc1, (d: any) => {
202
228
  d.title.insert(0, "Snapshot")
203
229
  d.count.set(42)
@@ -206,7 +232,7 @@ describe("createYjsDocFromEntirety", () => {
206
232
  change(doc1, (d: any) => d.items.push("b"))
207
233
 
208
234
  const payload = exportEntirety(doc1)
209
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
235
+ const doc2 = createDoc(boundSimple, payload)
210
236
 
211
237
  expect(doc2.title()).toBe("Snapshot")
212
238
  expect(doc2.count()).toBe(42)
@@ -214,7 +240,7 @@ describe("createYjsDocFromEntirety", () => {
214
240
  })
215
241
 
216
242
  it("reconstructs state after mutations", () => {
217
- const doc1 = createYjsDoc(SimpleSchema)
243
+ const doc1 = createDoc(boundSimple)
218
244
  change(doc1, (d: any) => {
219
245
  d.title.insert(0, "Start")
220
246
  })
@@ -226,7 +252,7 @@ describe("createYjsDocFromEntirety", () => {
226
252
  })
227
253
 
228
254
  const payload = exportEntirety(doc1)
229
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
255
+ const doc2 = createDoc(boundSimple, payload)
230
256
 
231
257
  expect(doc2.title()).toBe("Start End")
232
258
  expect(doc2.count()).toBe(99)
@@ -234,7 +260,7 @@ describe("createYjsDocFromEntirety", () => {
234
260
  })
235
261
 
236
262
  it("reconstructs nested struct state from snapshot", () => {
237
- const doc1 = createYjsDoc(NestedSchema)
263
+ const doc1 = createDoc(boundNested)
238
264
  change(doc1, (d: any) => {
239
265
  d.title.insert(0, "Nested")
240
266
  d.meta.author.set("Alice")
@@ -244,7 +270,7 @@ describe("createYjsDocFromEntirety", () => {
244
270
  change(doc1, (d: any) => d.meta.tags.push("v2"))
245
271
 
246
272
  const payload = exportEntirety(doc1)
247
- const doc2 = createYjsDocFromEntirety(NestedSchema, payload)
273
+ const doc2 = createDoc(boundNested, payload)
248
274
 
249
275
  expect(doc2.title()).toBe("Nested")
250
276
  expect(doc2.meta.author()).toBe("Alice")
@@ -254,12 +280,12 @@ describe("createYjsDocFromEntirety", () => {
254
280
  })
255
281
 
256
282
  it("reconstructs struct list state from snapshot", () => {
257
- const doc1 = createYjsDoc(StructListSchema)
283
+ const doc1 = createDoc(boundStructList)
258
284
  change(doc1, (d: any) => d.tasks.push({ name: "Task A", done: false }))
259
285
  change(doc1, (d: any) => d.tasks.push({ name: "Task B", done: true }))
260
286
 
261
287
  const payload = exportEntirety(doc1)
262
- const doc2 = createYjsDocFromEntirety(StructListSchema, payload)
288
+ const doc2 = createDoc(boundStructList, payload)
263
289
 
264
290
  expect(doc2.tasks.length).toBe(2)
265
291
  expect((doc2.tasks.at(0) as any).name()).toBe("Task A")
@@ -267,12 +293,12 @@ describe("createYjsDocFromEntirety", () => {
267
293
  })
268
294
 
269
295
  it("is writable after reconstruction", () => {
270
- const doc1 = createYjsDoc(SimpleSchema)
296
+ const doc1 = createDoc(boundSimple)
271
297
  change(doc1, (d: any) => {
272
298
  d.title.insert(0, "Original")
273
299
  })
274
300
  const payload = exportEntirety(doc1)
275
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
301
+ const doc2 = createDoc(boundSimple, payload)
276
302
 
277
303
  change(doc2, (d: any) => {
278
304
  d.title.insert(8, " Copy")
@@ -284,12 +310,12 @@ describe("createYjsDocFromEntirety", () => {
284
310
  })
285
311
 
286
312
  it("is observable after reconstruction", () => {
287
- const doc1 = createYjsDoc(SimpleSchema)
313
+ const doc1 = createDoc(boundSimple)
288
314
  change(doc1, (d: any) => {
289
315
  d.title.insert(0, "Original")
290
316
  })
291
317
  const payload = exportEntirety(doc1)
292
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
318
+ const doc2 = createDoc(boundSimple, payload)
293
319
 
294
320
  const received: any[] = []
295
321
  subscribe(doc2, (changeset: any) => {
@@ -311,13 +337,13 @@ describe("createYjsDocFromEntirety", () => {
311
337
  describe("sync primitives", () => {
312
338
  describe("version", () => {
313
339
  it("returns a YjsVersion", () => {
314
- const doc = createYjsDoc(SimpleSchema)
340
+ const doc = createDoc(boundSimple)
315
341
  const v = version(doc)
316
342
  expect(v).toBeInstanceOf(YjsVersion)
317
343
  })
318
344
 
319
345
  it("advances after mutations", () => {
320
- const doc = createYjsDoc(SimpleSchema)
346
+ const doc = createDoc(boundSimple)
321
347
  const v1 = version(doc)
322
348
 
323
349
  change(doc, (d: any) => {
@@ -329,7 +355,7 @@ describe("sync primitives", () => {
329
355
  })
330
356
 
331
357
  it("serialize/parse round-trips", () => {
332
- const doc = createYjsDoc(SimpleSchema)
358
+ const doc = createDoc(boundSimple)
333
359
  change(doc, (d: any) => {
334
360
  d.title.insert(0, "Test")
335
361
  })
@@ -342,7 +368,7 @@ describe("sync primitives", () => {
342
368
 
343
369
  describe("exportEntirety", () => {
344
370
  it("returns a binary payload", () => {
345
- const doc = createYjsDoc(SimpleSchema)
371
+ const doc = createDoc(boundSimple)
346
372
  change(doc, (d: any) => {
347
373
  d.title.insert(0, "Snap")
348
374
  })
@@ -355,11 +381,11 @@ describe("sync primitives", () => {
355
381
 
356
382
  describe("exportSince + merge", () => {
357
383
  it("syncs incremental changes between two docs", () => {
358
- const doc1 = createYjsDoc(SimpleSchema)
384
+ const doc1 = createDoc(boundSimple)
359
385
  change(doc1, (d: any) => {
360
386
  d.title.insert(0, "Start")
361
387
  })
362
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
388
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
363
389
 
364
390
  const v2Before = version(doc2)
365
391
 
@@ -373,7 +399,7 @@ describe("sync primitives", () => {
373
399
  // Export delta and apply to doc2
374
400
  const delta = exportSince(doc1, v2Before)
375
401
  expect(delta).not.toBeNull()
376
- expect(delta!.encoding).toBe("binary")
402
+ expect(delta?.encoding).toBe("binary")
377
403
 
378
404
  merge(doc2, delta!)
379
405
 
@@ -383,8 +409,8 @@ describe("sync primitives", () => {
383
409
  })
384
410
 
385
411
  it("syncs multiple incremental deltas", () => {
386
- const doc1 = createYjsDoc(SimpleSchema)
387
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
412
+ const doc1 = createDoc(boundSimple)
413
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
388
414
 
389
415
  // First round
390
416
  let vBefore = version(doc2)
@@ -412,11 +438,11 @@ describe("sync primitives", () => {
412
438
  })
413
439
 
414
440
  it("changefeed fires on merge", () => {
415
- const doc1 = createYjsDoc(SimpleSchema)
441
+ const doc1 = createDoc(boundSimple)
416
442
  change(doc1, (d: any) => {
417
443
  d.title.insert(0, "Source")
418
444
  })
419
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
445
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
420
446
 
421
447
  const v2Before = version(doc2)
422
448
 
@@ -438,8 +464,8 @@ describe("sync primitives", () => {
438
464
  })
439
465
 
440
466
  it("merge passes origin to changefeed", () => {
441
- const doc1 = createYjsDoc(SimpleSchema)
442
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
467
+ const doc1 = createDoc(boundSimple)
468
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
443
469
 
444
470
  const v2Before = version(doc2)
445
471
  change(doc1, (d: any) => {
@@ -459,7 +485,7 @@ describe("sync primitives", () => {
459
485
 
460
486
  describe("versions equal after sync", () => {
461
487
  it("versions equal after full snapshot sync", () => {
462
- const doc1 = createYjsDoc(SimpleSchema)
488
+ const doc1 = createDoc(boundSimple)
463
489
  change(doc1, (d: any) => {
464
490
  d.title.insert(0, "Same")
465
491
  })
@@ -467,14 +493,14 @@ describe("sync primitives", () => {
467
493
  d.count.set(42)
468
494
  })
469
495
 
470
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
496
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
471
497
 
472
498
  expect(version(doc1).compare(version(doc2))).toBe("equal")
473
499
  })
474
500
 
475
501
  it("versions equal after bidirectional delta sync", () => {
476
- const doc1 = createYjsDoc(SimpleSchema)
477
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
502
+ const doc1 = createDoc(boundSimple)
503
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
478
504
 
479
505
  const v1Before = version(doc1)
480
506
  const v2Before = version(doc2)
@@ -505,11 +531,8 @@ describe("sync primitives", () => {
505
531
  describe("full workflow", () => {
506
532
  it("create → mutate → sync → observe", () => {
507
533
  // 1. Create two docs
508
- const doc1 = createYjsDoc(StructListSchema)
509
- const doc2 = createYjsDocFromEntirety(
510
- StructListSchema,
511
- exportEntirety(doc1),
512
- )
534
+ const doc1 = createDoc(boundStructList)
535
+ const doc2 = createDoc(boundStructList, exportEntirety(doc1))
513
536
 
514
537
  // 2. Set up observer on doc2
515
538
  const changes: any[] = []
@@ -560,7 +583,7 @@ describe("full workflow", () => {
560
583
 
561
584
  it("create → mutate → snapshot → reconstruct → continue", () => {
562
585
  // 1. Create and mutate
563
- const doc1 = createYjsDoc(SimpleSchema)
586
+ const doc1 = createDoc(boundSimple)
564
587
  change(doc1, (d: any) => {
565
588
  d.title.insert(0, "Start")
566
589
  })
@@ -574,7 +597,7 @@ describe("full workflow", () => {
574
597
  const snapshot = exportEntirety(doc1)
575
598
 
576
599
  // 3. Reconstruct
577
- const doc2 = createYjsDocFromEntirety(SimpleSchema, snapshot)
600
+ const doc2 = createDoc(boundSimple, snapshot)
578
601
  expect(doc2.title()).toBe("Start Middle")
579
602
  expect(doc2.count()).toBe(10)
580
603
  expect(doc2.items()).toEqual(["first"])
@@ -599,8 +622,8 @@ describe("full workflow", () => {
599
622
 
600
623
  it("concurrent edits converge correctly", () => {
601
624
  // 1. Create two peers from the same initial state
602
- const doc1 = createYjsDoc(SimpleSchema)
603
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
625
+ const doc1 = createDoc(boundSimple)
626
+ const doc2 = createDoc(boundSimple, exportEntirety(doc1))
604
627
 
605
628
  const v1Before = version(doc1)
606
629
  const v2Before = version(doc2)
@@ -1,4 +1,4 @@
1
- import { RawPath, Schema } from "@kyneta/schema"
1
+ import { KIND, RawPath, Schema } from "@kyneta/schema"
2
2
  import { describe, expect, it } from "vitest"
3
3
  import * as Y from "yjs"
4
4
  import { ensureContainers } from "../populate.js"
@@ -15,10 +15,7 @@ import { yjsReader } from "../reader.js"
15
15
  * After `ensureContainers` the doc has the correct shared types but no
16
16
  * values. We populate values via raw Yjs API within a single transact.
17
17
  */
18
- function setup(
19
- schema: ReturnType<typeof Schema.doc>,
20
- seed?: Record<string, unknown>,
21
- ) {
18
+ function setup(schema: any, seed?: Record<string, unknown>) {
22
19
  const doc = new Y.Doc()
23
20
  ensureContainers(doc, schema)
24
21
  if (seed) {
@@ -42,11 +39,11 @@ function setup(
42
39
  */
43
40
  function populateSeed(
44
41
  ymap: Y.Map<unknown>,
45
- schema: ReturnType<typeof Schema.doc>,
42
+ schema: any,
46
43
  seed: Record<string, unknown>,
47
44
  ) {
48
- const rootProduct = unwrapToProduct(schema)
49
- if (!rootProduct) return
45
+ if (schema[KIND] !== "product") return
46
+ const rootProduct = schema
50
47
 
51
48
  for (const [key, value] of Object.entries(seed)) {
52
49
  if (value === undefined) continue
@@ -63,20 +60,15 @@ function populateField(
63
60
  fieldSchema: any,
64
61
  value: unknown,
65
62
  ) {
66
- const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : undefined
67
-
68
- if (tag === "text") {
69
- // Text field the Y.Text was already created by ensureContainers
70
- const text = ymap.get(key) as Y.Text
71
- if (text && typeof value === "string" && value.length > 0) {
72
- text.insert(0, value)
63
+ switch (fieldSchema[KIND]) {
64
+ case "text": {
65
+ // Text field the Y.Text was already created by ensureContainers
66
+ const text = ymap.get(key) as Y.Text
67
+ if (text && typeof value === "string" && value.length > 0) {
68
+ text.insert(0, value)
69
+ }
70
+ return
73
71
  }
74
- return
75
- }
76
-
77
- const structural = unwrapAnnotations(fieldSchema)
78
-
79
- switch (structural._kind) {
80
72
  case "product": {
81
73
  // Struct — recurse into the existing Y.Map
82
74
  const childMap = ymap.get(key) as Y.Map<unknown>
@@ -84,7 +76,7 @@ function populateField(
84
76
  for (const [childKey, childValue] of Object.entries(
85
77
  value as Record<string, unknown>,
86
78
  )) {
87
- const childFieldSchema = (structural.fields as Record<string, any>)[
79
+ const childFieldSchema = (fieldSchema.fields as Record<string, any>)[
88
80
  childKey
89
81
  ]
90
82
  if (!childFieldSchema) continue
@@ -99,11 +91,11 @@ function populateField(
99
91
  const arr = ymap.get(key) as Y.Array<unknown>
100
92
  if (arr && Array.isArray(value)) {
101
93
  for (const item of value) {
102
- const itemSchema = structural.item
103
- if (itemSchema && unwrapAnnotations(itemSchema)._kind === "product") {
94
+ const itemSchema = fieldSchema.item
95
+ if (itemSchema && itemSchema[KIND] === "product") {
104
96
  // Struct items: create a Y.Map for each
105
97
  const itemMap = buildStructMap(
106
- unwrapAnnotations(itemSchema),
98
+ itemSchema,
107
99
  item as Record<string, unknown>,
108
100
  )
109
101
  arr.push([itemMap])
@@ -150,22 +142,19 @@ function buildStructMap(
150
142
  const value = seed[key]
151
143
  if (value === undefined) continue
152
144
 
153
- const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : undefined
154
- if (tag === "text") {
155
- const text = new Y.Text()
156
- if (typeof value === "string" && value.length > 0) {
157
- text.insert(0, value)
145
+ switch (fieldSchema[KIND]) {
146
+ case "text": {
147
+ const text = new Y.Text()
148
+ if (typeof value === "string" && value.length > 0) {
149
+ text.insert(0, value)
150
+ }
151
+ map.set(key, text)
152
+ break
158
153
  }
159
- map.set(key, text)
160
- continue
161
- }
162
-
163
- const structural = unwrapAnnotations(fieldSchema)
164
- switch (structural._kind) {
165
154
  case "product": {
166
155
  map.set(
167
156
  key,
168
- buildStructMap(structural, value as Record<string, unknown>),
157
+ buildStructMap(fieldSchema, value as Record<string, unknown>),
169
158
  )
170
159
  break
171
160
  }
@@ -173,16 +162,10 @@ function buildStructMap(
173
162
  const arr = new Y.Array()
174
163
  if (Array.isArray(value)) {
175
164
  for (const item of value) {
176
- const itemSchema = structural.element ?? structural.schema
177
- if (
178
- itemSchema &&
179
- unwrapAnnotations(itemSchema)._kind === "product"
180
- ) {
165
+ const itemSchema = fieldSchema.item
166
+ if (itemSchema && itemSchema[KIND] === "product") {
181
167
  arr.push([
182
- buildStructMap(
183
- unwrapAnnotations(itemSchema),
184
- item as Record<string, unknown>,
185
- ),
168
+ buildStructMap(itemSchema, item as Record<string, unknown>),
186
169
  ])
187
170
  } else {
188
171
  arr.push([item])
@@ -212,23 +195,6 @@ function buildStructMap(
212
195
  return map
213
196
  }
214
197
 
215
- function unwrapToProduct(schema: any): any {
216
- let s = schema
217
- while (s._kind === "annotated" && s.schema !== undefined) {
218
- s = s.schema
219
- }
220
- if (s._kind === "product") return s
221
- return null
222
- }
223
-
224
- function unwrapAnnotations(schema: any): any {
225
- let s = schema
226
- while (s._kind === "annotated" && s.schema !== undefined) {
227
- s = s.schema
228
- }
229
- return s
230
- }
231
-
232
198
  /** Build a RawPath from variadic key/index segments. */
233
199
  function p(...segs: (string | number)[]): RawPath {
234
200
  let path = RawPath.empty
@@ -242,18 +208,18 @@ function p(...segs: (string | number)[]): RawPath {
242
208
  // Schemas used across tests
243
209
  // ===========================================================================
244
210
 
245
- const TextSchema = Schema.doc({
246
- title: Schema.annotated("text"),
247
- subtitle: Schema.annotated("text"),
211
+ const TextSchema = Schema.struct({
212
+ title: Schema.text(),
213
+ subtitle: Schema.text(),
248
214
  })
249
215
 
250
- const ScalarSchema = Schema.doc({
216
+ const ScalarSchema = Schema.struct({
251
217
  name: Schema.string(),
252
218
  count: Schema.number(),
253
219
  active: Schema.boolean(),
254
220
  })
255
221
 
256
- const NestedStructSchema = Schema.doc({
222
+ const NestedStructSchema = Schema.struct({
257
223
  profile: Schema.struct({
258
224
  first: Schema.string(),
259
225
  last: Schema.string(),
@@ -264,7 +230,7 @@ const NestedStructSchema = Schema.doc({
264
230
  }),
265
231
  })
266
232
 
267
- const ListSchema = Schema.doc({
233
+ const ListSchema = Schema.struct({
268
234
  items: Schema.list(Schema.string()),
269
235
  structs: Schema.list(
270
236
  Schema.struct({
@@ -274,12 +240,12 @@ const ListSchema = Schema.doc({
274
240
  ),
275
241
  })
276
242
 
277
- const MapSchema = Schema.doc({
243
+ const MapSchema = Schema.struct({
278
244
  labels: Schema.record(Schema.string()),
279
245
  })
280
246
 
281
- const MixedSchema = Schema.doc({
282
- title: Schema.annotated("text"),
247
+ const MixedSchema = Schema.struct({
248
+ title: Schema.text(),
283
249
  count: Schema.number(),
284
250
  items: Schema.list(
285
251
  Schema.struct({