@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,10 +1,10 @@
1
1
  // record-text-spike — validate text-inside-struct patterns for Yjs backend.
2
2
  //
3
3
  // The Yjs analog of the Loro counter-in-record spike. Yjs doesn't support
4
- // counter annotations, but it DOES support text annotations. The same
5
- // structural bug exists: when a struct is dynamically inserted into a
6
- // record or list via .set() or .push(), text fields declared in the schema
7
- // but missing from the value object don't get Y.Text containers created.
4
+ // counters, but it DOES support text. The same structural bug exists: when
5
+ // a struct is dynamically inserted into a record or list via .set() or
6
+ // .push(), text fields declared in the schema but missing from the value
7
+ // object don't get Y.Text containers created.
8
8
  //
9
9
  // This spike tests:
10
10
  // 1. record(struct({ name: string(), bio: text() }))
@@ -16,34 +16,33 @@
16
16
  // 3. Reading back: doc.profiles() should return { [key]: { name, bio } }
17
17
  // 4. Sync: two peers should converge after exchanging deltas
18
18
 
19
- import { describe, expect, it } from "vitest"
20
19
  import {
21
20
  change,
22
- createYjsDoc,
23
- createYjsDocFromEntirety,
21
+ createDoc,
24
22
  exportEntirety,
25
23
  exportSince,
26
24
  merge,
27
25
  Schema,
28
26
  subscribe,
29
- text,
30
27
  version,
31
- } from "../index.js"
28
+ } from "@kyneta/schema"
29
+ import { describe, expect, it } from "vitest"
30
+ import { yjs } from "../bind-yjs.js"
32
31
 
33
32
  // ===========================================================================
34
33
  // Schemas
35
34
  // ===========================================================================
36
35
 
37
- const ProfileSchema = Schema.doc({
36
+ const ProfileSchema = Schema.struct({
38
37
  profiles: Schema.record(
39
38
  Schema.struct({
40
39
  displayName: Schema.string(),
41
- bio: text(),
40
+ bio: Schema.text(),
42
41
  }),
43
42
  ),
44
43
  })
45
44
 
46
- const PlainRecordSchema = Schema.doc({
45
+ const PlainRecordSchema = Schema.struct({
47
46
  profiles: Schema.record(
48
47
  Schema.struct({
49
48
  displayName: Schema.string(),
@@ -52,22 +51,30 @@ const PlainRecordSchema = Schema.doc({
52
51
  ),
53
52
  })
54
53
 
55
- const ListProfileSchema = Schema.doc({
54
+ const ListProfileSchema = Schema.struct({
56
55
  players: Schema.list(
57
56
  Schema.struct({
58
57
  name: Schema.string(),
59
- bio: text(),
58
+ bio: Schema.text(),
60
59
  }),
61
60
  ),
62
61
  })
63
62
 
63
+ // ===========================================================================
64
+ // Bound schemas
65
+ // ===========================================================================
66
+
67
+ const BoundProfile = yjs.bind(ProfileSchema)
68
+ const BoundPlainRecord = yjs.bind(PlainRecordSchema)
69
+ const BoundListProfile = yjs.bind(ListProfileSchema)
70
+
64
71
  // ===========================================================================
65
72
  // Baseline: record-of-struct (plain, no text)
66
73
  // ===========================================================================
67
74
 
68
75
  describe("record-of-struct (plain baseline)", () => {
69
76
  it("set a record entry and read it back", () => {
70
- const doc = createYjsDoc(PlainRecordSchema)
77
+ const doc = createDoc(BoundPlainRecord)
71
78
 
72
79
  change(doc, (d: any) => {
73
80
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -80,7 +87,7 @@ describe("record-of-struct (plain baseline)", () => {
80
87
  })
81
88
 
82
89
  it("set multiple entries and read all back", () => {
83
- const doc = createYjsDoc(PlainRecordSchema)
90
+ const doc = createDoc(BoundPlainRecord)
84
91
 
85
92
  change(doc, (d: any) => {
86
93
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -95,7 +102,7 @@ describe("record-of-struct (plain baseline)", () => {
95
102
  })
96
103
 
97
104
  it("navigate into a record entry via .at()", () => {
98
- const doc = createYjsDoc(PlainRecordSchema)
105
+ const doc = createDoc(BoundPlainRecord)
99
106
 
100
107
  change(doc, (d: any) => {
101
108
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -108,14 +115,14 @@ describe("record-of-struct (plain baseline)", () => {
108
115
  })
109
116
 
110
117
  it("syncs record-of-struct between two peers via snapshot", () => {
111
- const docA = createYjsDoc(PlainRecordSchema)
118
+ const docA = createDoc(BoundPlainRecord)
112
119
 
113
120
  change(docA, (d: any) => {
114
121
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
115
122
  })
116
123
 
117
124
  const snapshot = exportEntirety(docA)
118
- const docB = createYjsDocFromEntirety(PlainRecordSchema, snapshot)
125
+ const docB = createDoc(BoundPlainRecord, snapshot)
119
126
 
120
127
  expect(docB.profiles()).toEqual({
121
128
  alice: { displayName: "Alice", age: 30 },
@@ -123,17 +130,14 @@ describe("record-of-struct (plain baseline)", () => {
123
130
  })
124
131
 
125
132
  it("syncs record-of-struct between two peers via delta", () => {
126
- const docA = createYjsDoc(PlainRecordSchema)
133
+ const docA = createDoc(BoundPlainRecord)
127
134
 
128
135
  change(docA, (d: any) => {
129
136
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
130
137
  })
131
138
 
132
139
  // Establish docB from snapshot (avoids Yjs clientID collision)
133
- const docB = createYjsDocFromEntirety(
134
- PlainRecordSchema,
135
- exportEntirety(docA),
136
- )
140
+ const docB = createDoc(BoundPlainRecord, exportEntirety(docA))
137
141
 
138
142
  const v0 = version(docB)
139
143
 
@@ -158,7 +162,7 @@ describe("record-of-struct (plain baseline)", () => {
158
162
 
159
163
  describe("text-inside-struct-inside-record", () => {
160
164
  it("set a record entry with text field omitted and read it back", () => {
161
- const doc = createYjsDoc(ProfileSchema)
165
+ const doc = createDoc(BoundProfile)
162
166
 
163
167
  change(doc, (d: any) => {
164
168
  d.profiles.set("alice", { displayName: "Alice" })
@@ -171,7 +175,7 @@ describe("text-inside-struct-inside-record", () => {
171
175
  })
172
176
 
173
177
  it("set a record entry with text field provided and read it back", () => {
174
- const doc = createYjsDoc(ProfileSchema)
178
+ const doc = createDoc(BoundProfile)
175
179
 
176
180
  change(doc, (d: any) => {
177
181
  d.profiles.set("alice", { displayName: "Alice", bio: "Hello world" })
@@ -184,7 +188,7 @@ describe("text-inside-struct-inside-record", () => {
184
188
  })
185
189
 
186
190
  it("navigate into a record entry and read the text", () => {
187
- const doc = createYjsDoc(ProfileSchema)
191
+ const doc = createDoc(BoundProfile)
188
192
 
189
193
  change(doc, (d: any) => {
190
194
  d.profiles.set("alice", { displayName: "Alice" })
@@ -197,7 +201,7 @@ describe("text-inside-struct-inside-record", () => {
197
201
  })
198
202
 
199
203
  it("insert text into a text field inside a record entry (field omitted at creation)", () => {
200
- const doc = createYjsDoc(ProfileSchema)
204
+ const doc = createDoc(BoundProfile)
201
205
 
202
206
  change(doc, (d: any) => {
203
207
  d.profiles.set("alice", { displayName: "Alice" })
@@ -211,7 +215,7 @@ describe("text-inside-struct-inside-record", () => {
211
215
  })
212
216
 
213
217
  it("insert text into a text field inside a record entry (field provided at creation)", () => {
214
- const doc = createYjsDoc(ProfileSchema)
218
+ const doc = createDoc(BoundProfile)
215
219
 
216
220
  change(doc, (d: any) => {
217
221
  d.profiles.set("alice", { displayName: "Alice", bio: "Initial" })
@@ -225,7 +229,7 @@ describe("text-inside-struct-inside-record", () => {
225
229
  })
226
230
 
227
231
  it("set multiple entries and edit text independently", () => {
228
- const doc = createYjsDoc(ProfileSchema)
232
+ const doc = createDoc(BoundProfile)
229
233
 
230
234
  change(doc, (d: any) => {
231
235
  d.profiles.set("alice", { displayName: "Alice" })
@@ -242,7 +246,7 @@ describe("text-inside-struct-inside-record", () => {
242
246
  })
243
247
 
244
248
  it("subscribe fires on text edit inside record entry", () => {
245
- const doc = createYjsDoc(ProfileSchema)
249
+ const doc = createDoc(BoundProfile)
246
250
 
247
251
  change(doc, (d: any) => {
248
252
  d.profiles.set("alice", { displayName: "Alice" })
@@ -261,7 +265,7 @@ describe("text-inside-struct-inside-record", () => {
261
265
  })
262
266
 
263
267
  it("syncs text-inside-record via snapshot", () => {
264
- const docA = createYjsDoc(ProfileSchema)
268
+ const docA = createDoc(BoundProfile)
265
269
 
266
270
  change(docA, (d: any) => {
267
271
  d.profiles.set("alice", { displayName: "Alice" })
@@ -271,7 +275,7 @@ describe("text-inside-struct-inside-record", () => {
271
275
  })
272
276
 
273
277
  const snapshot = exportEntirety(docA)
274
- const docB = createYjsDocFromEntirety(ProfileSchema, snapshot)
278
+ const docB = createDoc(BoundProfile, snapshot)
275
279
 
276
280
  expect(docB.profiles()).toEqual({
277
281
  alice: { displayName: "Alice", bio: "Collaborative bio" },
@@ -282,7 +286,7 @@ describe("text-inside-struct-inside-record", () => {
282
286
  })
283
287
 
284
288
  it("syncs text-inside-record via delta", () => {
285
- const docA = createYjsDoc(ProfileSchema)
289
+ const docA = createDoc(BoundProfile)
286
290
 
287
291
  change(docA, (d: any) => {
288
292
  d.profiles.set("alice", { displayName: "Alice" })
@@ -291,7 +295,7 @@ describe("text-inside-struct-inside-record", () => {
291
295
  // Establish docB from snapshot (consistent with Yjs test patterns —
292
296
  // two independently-created Y.Docs may share a clientID, causing
293
297
  // silent update drops)
294
- const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
298
+ const docB = createDoc(BoundProfile, exportEntirety(docA))
295
299
  const v0 = version(docB)
296
300
 
297
301
  change(docA, (d: any) => {
@@ -314,13 +318,13 @@ describe("text-inside-struct-inside-record", () => {
314
318
  })
315
319
 
316
320
  it("concurrent text edits inside record entries converge", () => {
317
- const docA = createYjsDoc(ProfileSchema)
321
+ const docA = createDoc(BoundProfile)
318
322
 
319
323
  // Sync initial state: A creates the entry, B starts from snapshot
320
324
  change(docA, (d: any) => {
321
325
  d.profiles.set("alice", { displayName: "Alice" })
322
326
  })
323
- const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
327
+ const docB = createDoc(BoundProfile, exportEntirety(docA))
324
328
 
325
329
  // Both peers edit concurrently
326
330
  const vA = version(docA)
@@ -354,7 +358,7 @@ describe("text-inside-struct-inside-record", () => {
354
358
 
355
359
  describe("text-inside-struct-inside-list", () => {
356
360
  it("push a struct with text field omitted and read it back", () => {
357
- const doc = createYjsDoc(ListProfileSchema)
361
+ const doc = createDoc(BoundListProfile)
358
362
 
359
363
  change(doc, (d: any) => {
360
364
  d.players.push({ name: "Alice" })
@@ -366,7 +370,7 @@ describe("text-inside-struct-inside-list", () => {
366
370
  })
367
371
 
368
372
  it("push a struct with text field provided and read it back", () => {
369
- const doc = createYjsDoc(ListProfileSchema)
373
+ const doc = createDoc(BoundListProfile)
370
374
 
371
375
  change(doc, (d: any) => {
372
376
  d.players.push({ name: "Alice", bio: "Hi there" })
@@ -378,7 +382,7 @@ describe("text-inside-struct-inside-list", () => {
378
382
  })
379
383
 
380
384
  it("insert text into a text field inside a list item (field omitted at creation)", () => {
381
- const doc = createYjsDoc(ListProfileSchema)
385
+ const doc = createDoc(BoundListProfile)
382
386
 
383
387
  change(doc, (d: any) => {
384
388
  d.players.push({ name: "Alice" })
@@ -392,7 +396,7 @@ describe("text-inside-struct-inside-list", () => {
392
396
  })
393
397
 
394
398
  it("multiple list items with independent text fields", () => {
395
- const doc = createYjsDoc(ListProfileSchema)
399
+ const doc = createDoc(BoundListProfile)
396
400
 
397
401
  change(doc, (d: any) => {
398
402
  d.players.push({ name: "Alice" })
@@ -409,17 +413,14 @@ describe("text-inside-struct-inside-list", () => {
409
413
  })
410
414
 
411
415
  it("syncs list-of-struct-with-text via delta", () => {
412
- const docA = createYjsDoc(ListProfileSchema)
416
+ const docA = createDoc(BoundListProfile)
413
417
 
414
418
  change(docA, (d: any) => {
415
419
  d.players.push({ name: "Alice" })
416
420
  })
417
421
 
418
422
  // Establish docB from snapshot (avoids Yjs clientID collision)
419
- const docB = createYjsDocFromEntirety(
420
- ListProfileSchema,
421
- exportEntirety(docA),
422
- )
423
+ const docB = createDoc(BoundListProfile, exportEntirety(docA))
423
424
  const v0 = version(docB)
424
425
 
425
426
  change(docA, (d: any) => {
@@ -9,16 +9,16 @@
9
9
  import { BACKING_DOC, Schema, STRUCTURAL_YJS_CLIENT_ID } from "@kyneta/schema"
10
10
  import { describe, expect, it } from "vitest"
11
11
  import * as Y from "yjs"
12
- import { bindYjs } from "../bind-yjs.js"
12
+ import { yjs } from "../bind-yjs.js"
13
13
  import { ensureContainers } from "../populate.js"
14
- import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
14
+ import { yjsSubstrateFactory } from "../substrate.js"
15
15
 
16
16
  // ===========================================================================
17
17
  // Schemas used across tests
18
18
  // ===========================================================================
19
19
 
20
- const TestSchema = Schema.doc({
21
- title: Schema.annotated("text"),
20
+ const TestSchema = Schema.struct({
21
+ title: Schema.text(),
22
22
  count: Schema.number(),
23
23
  items: Schema.list(Schema.string()),
24
24
  })
@@ -138,20 +138,20 @@ describe("structural merge protocol (Yjs)", () => {
138
138
 
139
139
  it("field reordering in source doesn't affect structural ops", () => {
140
140
  // Schema A: fields in one order
141
- const schemaA = Schema.doc({
141
+ const schemaA = Schema.struct({
142
142
  alpha: Schema.string(),
143
143
  beta: Schema.number(),
144
- gamma: Schema.annotated("text"),
144
+ gamma: Schema.text(),
145
145
  })
146
146
 
147
147
  // Schema B: same fields, different insertion order
148
148
  // JavaScript objects preserve insertion order, so we construct
149
149
  // with a different order to verify alphabetical sort overrides it.
150
150
  const fields: Record<string, any> = {}
151
- fields.gamma = Schema.annotated("text")
151
+ fields.gamma = Schema.text()
152
152
  fields.alpha = Schema.string()
153
153
  fields.beta = Schema.number()
154
- const schemaB = Schema.doc(fields)
154
+ const schemaB = Schema.struct(fields)
155
155
 
156
156
  const docA = new Y.Doc()
157
157
  ensureContainers(docA, schemaA)
@@ -168,15 +168,15 @@ describe("structural merge protocol (Yjs)", () => {
168
168
  // ── Schema evolution ──
169
169
 
170
170
  it("add field after hydration, structural ops extend correctly", () => {
171
- const v1Schema = Schema.doc({
172
- title: Schema.annotated("text"),
171
+ const v1Schema = Schema.struct({
172
+ title: Schema.text(),
173
173
  count: Schema.number(),
174
174
  })
175
175
 
176
- const v2Schema = Schema.doc({
176
+ const v2Schema = Schema.struct({
177
177
  count: Schema.number(),
178
- notes: Schema.annotated("text"), // new field
179
- title: Schema.annotated("text"),
178
+ notes: Schema.text(), // new field
179
+ title: Schema.text(),
180
180
  })
181
181
 
182
182
  // Peer A: create v1, write data, export
@@ -282,10 +282,10 @@ describe("structural merge protocol (Yjs)", () => {
282
282
  expect(root2.get("count")).toBe(123)
283
283
  })
284
284
 
285
- // ── bindYjs integration ──
285
+ // ── yjs.bind integration ──
286
286
 
287
- it("bindYjs factory produces deterministic structural ops across peers", () => {
288
- const bound = bindYjs(TestSchema)
287
+ it("yjs.bind factory produces deterministic structural ops across peers", () => {
288
+ const bound = yjs.bind(TestSchema)
289
289
 
290
290
  const factoryA = bound.factory({ peerId: "alice" })
291
291
  const factoryB = bound.factory({ peerId: "bob" })
@@ -301,8 +301,8 @@ describe("structural merge protocol (Yjs)", () => {
301
301
  expect(stateA.data).toEqual(stateB.data)
302
302
  })
303
303
 
304
- it("bindYjs peers merge without structural conflict", () => {
305
- const bound = bindYjs(TestSchema)
304
+ it("yjs.bind peers merge without structural conflict", () => {
305
+ const bound = yjs.bind(TestSchema)
306
306
 
307
307
  const factoryA = bound.factory({ peerId: "alice" })
308
308
  const factoryB = bound.factory({ peerId: "bob" })