@kyneta/yjs-schema 1.2.0 → 1.3.1

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.
@@ -16,18 +16,18 @@
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
27
  version,
30
- } from "../index.js"
28
+ } from "@kyneta/schema"
29
+ import { describe, expect, it } from "vitest"
30
+ import { yjs } from "../bind-yjs.js"
31
31
 
32
32
  // ===========================================================================
33
33
  // Schemas
@@ -60,13 +60,21 @@ const ListProfileSchema = Schema.struct({
60
60
  ),
61
61
  })
62
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
+
63
71
  // ===========================================================================
64
72
  // Baseline: record-of-struct (plain, no text)
65
73
  // ===========================================================================
66
74
 
67
75
  describe("record-of-struct (plain baseline)", () => {
68
76
  it("set a record entry and read it back", () => {
69
- const doc = createYjsDoc(PlainRecordSchema)
77
+ const doc = createDoc(BoundPlainRecord)
70
78
 
71
79
  change(doc, (d: any) => {
72
80
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -79,7 +87,7 @@ describe("record-of-struct (plain baseline)", () => {
79
87
  })
80
88
 
81
89
  it("set multiple entries and read all back", () => {
82
- const doc = createYjsDoc(PlainRecordSchema)
90
+ const doc = createDoc(BoundPlainRecord)
83
91
 
84
92
  change(doc, (d: any) => {
85
93
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -94,7 +102,7 @@ describe("record-of-struct (plain baseline)", () => {
94
102
  })
95
103
 
96
104
  it("navigate into a record entry via .at()", () => {
97
- const doc = createYjsDoc(PlainRecordSchema)
105
+ const doc = createDoc(BoundPlainRecord)
98
106
 
99
107
  change(doc, (d: any) => {
100
108
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
@@ -107,14 +115,14 @@ describe("record-of-struct (plain baseline)", () => {
107
115
  })
108
116
 
109
117
  it("syncs record-of-struct between two peers via snapshot", () => {
110
- const docA = createYjsDoc(PlainRecordSchema)
118
+ const docA = createDoc(BoundPlainRecord)
111
119
 
112
120
  change(docA, (d: any) => {
113
121
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
114
122
  })
115
123
 
116
124
  const snapshot = exportEntirety(docA)
117
- const docB = createYjsDocFromEntirety(PlainRecordSchema, snapshot)
125
+ const docB = createDoc(BoundPlainRecord, snapshot)
118
126
 
119
127
  expect(docB.profiles()).toEqual({
120
128
  alice: { displayName: "Alice", age: 30 },
@@ -122,17 +130,14 @@ describe("record-of-struct (plain baseline)", () => {
122
130
  })
123
131
 
124
132
  it("syncs record-of-struct between two peers via delta", () => {
125
- const docA = createYjsDoc(PlainRecordSchema)
133
+ const docA = createDoc(BoundPlainRecord)
126
134
 
127
135
  change(docA, (d: any) => {
128
136
  d.profiles.set("alice", { displayName: "Alice", age: 30 })
129
137
  })
130
138
 
131
139
  // Establish docB from snapshot (avoids Yjs clientID collision)
132
- const docB = createYjsDocFromEntirety(
133
- PlainRecordSchema,
134
- exportEntirety(docA),
135
- )
140
+ const docB = createDoc(BoundPlainRecord, exportEntirety(docA))
136
141
 
137
142
  const v0 = version(docB)
138
143
 
@@ -157,7 +162,7 @@ describe("record-of-struct (plain baseline)", () => {
157
162
 
158
163
  describe("text-inside-struct-inside-record", () => {
159
164
  it("set a record entry with text field omitted and read it back", () => {
160
- const doc = createYjsDoc(ProfileSchema)
165
+ const doc = createDoc(BoundProfile)
161
166
 
162
167
  change(doc, (d: any) => {
163
168
  d.profiles.set("alice", { displayName: "Alice" })
@@ -170,7 +175,7 @@ describe("text-inside-struct-inside-record", () => {
170
175
  })
171
176
 
172
177
  it("set a record entry with text field provided and read it back", () => {
173
- const doc = createYjsDoc(ProfileSchema)
178
+ const doc = createDoc(BoundProfile)
174
179
 
175
180
  change(doc, (d: any) => {
176
181
  d.profiles.set("alice", { displayName: "Alice", bio: "Hello world" })
@@ -183,7 +188,7 @@ describe("text-inside-struct-inside-record", () => {
183
188
  })
184
189
 
185
190
  it("navigate into a record entry and read the text", () => {
186
- const doc = createYjsDoc(ProfileSchema)
191
+ const doc = createDoc(BoundProfile)
187
192
 
188
193
  change(doc, (d: any) => {
189
194
  d.profiles.set("alice", { displayName: "Alice" })
@@ -196,7 +201,7 @@ describe("text-inside-struct-inside-record", () => {
196
201
  })
197
202
 
198
203
  it("insert text into a text field inside a record entry (field omitted at creation)", () => {
199
- const doc = createYjsDoc(ProfileSchema)
204
+ const doc = createDoc(BoundProfile)
200
205
 
201
206
  change(doc, (d: any) => {
202
207
  d.profiles.set("alice", { displayName: "Alice" })
@@ -210,7 +215,7 @@ describe("text-inside-struct-inside-record", () => {
210
215
  })
211
216
 
212
217
  it("insert text into a text field inside a record entry (field provided at creation)", () => {
213
- const doc = createYjsDoc(ProfileSchema)
218
+ const doc = createDoc(BoundProfile)
214
219
 
215
220
  change(doc, (d: any) => {
216
221
  d.profiles.set("alice", { displayName: "Alice", bio: "Initial" })
@@ -224,7 +229,7 @@ describe("text-inside-struct-inside-record", () => {
224
229
  })
225
230
 
226
231
  it("set multiple entries and edit text independently", () => {
227
- const doc = createYjsDoc(ProfileSchema)
232
+ const doc = createDoc(BoundProfile)
228
233
 
229
234
  change(doc, (d: any) => {
230
235
  d.profiles.set("alice", { displayName: "Alice" })
@@ -241,7 +246,7 @@ describe("text-inside-struct-inside-record", () => {
241
246
  })
242
247
 
243
248
  it("subscribe fires on text edit inside record entry", () => {
244
- const doc = createYjsDoc(ProfileSchema)
249
+ const doc = createDoc(BoundProfile)
245
250
 
246
251
  change(doc, (d: any) => {
247
252
  d.profiles.set("alice", { displayName: "Alice" })
@@ -260,7 +265,7 @@ describe("text-inside-struct-inside-record", () => {
260
265
  })
261
266
 
262
267
  it("syncs text-inside-record via snapshot", () => {
263
- const docA = createYjsDoc(ProfileSchema)
268
+ const docA = createDoc(BoundProfile)
264
269
 
265
270
  change(docA, (d: any) => {
266
271
  d.profiles.set("alice", { displayName: "Alice" })
@@ -270,7 +275,7 @@ describe("text-inside-struct-inside-record", () => {
270
275
  })
271
276
 
272
277
  const snapshot = exportEntirety(docA)
273
- const docB = createYjsDocFromEntirety(ProfileSchema, snapshot)
278
+ const docB = createDoc(BoundProfile, snapshot)
274
279
 
275
280
  expect(docB.profiles()).toEqual({
276
281
  alice: { displayName: "Alice", bio: "Collaborative bio" },
@@ -281,7 +286,7 @@ describe("text-inside-struct-inside-record", () => {
281
286
  })
282
287
 
283
288
  it("syncs text-inside-record via delta", () => {
284
- const docA = createYjsDoc(ProfileSchema)
289
+ const docA = createDoc(BoundProfile)
285
290
 
286
291
  change(docA, (d: any) => {
287
292
  d.profiles.set("alice", { displayName: "Alice" })
@@ -290,7 +295,7 @@ describe("text-inside-struct-inside-record", () => {
290
295
  // Establish docB from snapshot (consistent with Yjs test patterns —
291
296
  // two independently-created Y.Docs may share a clientID, causing
292
297
  // silent update drops)
293
- const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
298
+ const docB = createDoc(BoundProfile, exportEntirety(docA))
294
299
  const v0 = version(docB)
295
300
 
296
301
  change(docA, (d: any) => {
@@ -313,13 +318,13 @@ describe("text-inside-struct-inside-record", () => {
313
318
  })
314
319
 
315
320
  it("concurrent text edits inside record entries converge", () => {
316
- const docA = createYjsDoc(ProfileSchema)
321
+ const docA = createDoc(BoundProfile)
317
322
 
318
323
  // Sync initial state: A creates the entry, B starts from snapshot
319
324
  change(docA, (d: any) => {
320
325
  d.profiles.set("alice", { displayName: "Alice" })
321
326
  })
322
- const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
327
+ const docB = createDoc(BoundProfile, exportEntirety(docA))
323
328
 
324
329
  // Both peers edit concurrently
325
330
  const vA = version(docA)
@@ -353,7 +358,7 @@ describe("text-inside-struct-inside-record", () => {
353
358
 
354
359
  describe("text-inside-struct-inside-list", () => {
355
360
  it("push a struct with text field omitted and read it back", () => {
356
- const doc = createYjsDoc(ListProfileSchema)
361
+ const doc = createDoc(BoundListProfile)
357
362
 
358
363
  change(doc, (d: any) => {
359
364
  d.players.push({ name: "Alice" })
@@ -365,7 +370,7 @@ describe("text-inside-struct-inside-list", () => {
365
370
  })
366
371
 
367
372
  it("push a struct with text field provided and read it back", () => {
368
- const doc = createYjsDoc(ListProfileSchema)
373
+ const doc = createDoc(BoundListProfile)
369
374
 
370
375
  change(doc, (d: any) => {
371
376
  d.players.push({ name: "Alice", bio: "Hi there" })
@@ -377,7 +382,7 @@ describe("text-inside-struct-inside-list", () => {
377
382
  })
378
383
 
379
384
  it("insert text into a text field inside a list item (field omitted at creation)", () => {
380
- const doc = createYjsDoc(ListProfileSchema)
385
+ const doc = createDoc(BoundListProfile)
381
386
 
382
387
  change(doc, (d: any) => {
383
388
  d.players.push({ name: "Alice" })
@@ -391,7 +396,7 @@ describe("text-inside-struct-inside-list", () => {
391
396
  })
392
397
 
393
398
  it("multiple list items with independent text fields", () => {
394
- const doc = createYjsDoc(ListProfileSchema)
399
+ const doc = createDoc(BoundListProfile)
395
400
 
396
401
  change(doc, (d: any) => {
397
402
  d.players.push({ name: "Alice" })
@@ -408,17 +413,14 @@ describe("text-inside-struct-inside-list", () => {
408
413
  })
409
414
 
410
415
  it("syncs list-of-struct-with-text via delta", () => {
411
- const docA = createYjsDoc(ListProfileSchema)
416
+ const docA = createDoc(BoundListProfile)
412
417
 
413
418
  change(docA, (d: any) => {
414
419
  d.players.push({ name: "Alice" })
415
420
  })
416
421
 
417
422
  // Establish docB from snapshot (avoids Yjs clientID collision)
418
- const docB = createYjsDocFromEntirety(
419
- ListProfileSchema,
420
- exportEntirety(docA),
421
- )
423
+ const docB = createDoc(BoundListProfile, exportEntirety(docA))
422
424
  const v0 = version(docB)
423
425
 
424
426
  change(docA, (d: any) => {
@@ -1,10 +1,20 @@
1
- import { change, RawPath, Schema, subscribe } from "@kyneta/schema"
1
+ import {
2
+ change,
3
+ createDoc,
4
+ createRef,
5
+ exportEntirety,
6
+ exportSince,
7
+ merge,
8
+ RawPath,
9
+ Schema,
10
+ subscribe,
11
+ version,
12
+ } from "@kyneta/schema"
2
13
  import { describe, expect, it } from "vitest"
3
14
  import * as Y from "yjs"
4
- import { createYjsDoc, createYjsDocFromEntirety } from "../create.js"
15
+ import { yjs } from "../bind-yjs.js"
5
16
  import { ensureContainers } from "../populate.js"
6
- import { yjsSubstrateFactory } from "../substrate.js"
7
- import { exportEntirety, exportSince, merge, version } from "../sync.js"
17
+ import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
8
18
  import { YjsVersion } from "../version.js"
9
19
 
10
20
  // ===========================================================================
@@ -66,7 +76,7 @@ describe("YjsSubstrate", () => {
66
76
  })
67
77
 
68
78
  it("creates a substrate and populates via change()", () => {
69
- const doc = createYjsDoc(SimpleSchema)
79
+ const doc = createDoc(yjs.bind(SimpleSchema))
70
80
  change(doc, (d: any) => {
71
81
  d.title.insert(0, "Hello")
72
82
  d.count.set(42)
@@ -81,7 +91,7 @@ describe("YjsSubstrate", () => {
81
91
  })
82
92
 
83
93
  it("creates a substrate with partial values (unset fields stay empty)", () => {
84
- const doc = createYjsDoc(SimpleSchema)
94
+ const doc = createDoc(yjs.bind(SimpleSchema))
85
95
  change(doc, (d: any) => {
86
96
  d.title.insert(0, "Partial")
87
97
  })
@@ -91,7 +101,7 @@ describe("YjsSubstrate", () => {
91
101
  })
92
102
 
93
103
  it("creates a substrate with nested struct values via change()", () => {
94
- const doc = createYjsDoc(FullSchema)
104
+ const doc = createDoc(yjs.bind(FullSchema))
95
105
  change(doc, (d: any) => {
96
106
  d.meta.author.set("Alice")
97
107
  })
@@ -99,7 +109,7 @@ describe("YjsSubstrate", () => {
99
109
  })
100
110
 
101
111
  it("creates a substrate with struct list values via change()", () => {
102
- const doc = createYjsDoc(StructListSchema)
112
+ const doc = createDoc(yjs.bind(StructListSchema))
103
113
  // Separate change() calls for list pushes to preserve order
104
114
  change(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
105
115
  change(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
@@ -114,7 +124,7 @@ describe("YjsSubstrate", () => {
114
124
 
115
125
  describe("write round-trip", () => {
116
126
  it("text insert round-trips through prepare/flush", () => {
117
- const doc = createYjsDoc(SimpleSchema)
127
+ const doc = createDoc(yjs.bind(SimpleSchema))
118
128
  change(doc, (d: any) => {
119
129
  d.title.insert(0, "Hello")
120
130
  })
@@ -122,7 +132,7 @@ describe("YjsSubstrate", () => {
122
132
  })
123
133
 
124
134
  it("scalar set round-trips through prepare/flush", () => {
125
- const doc = createYjsDoc(SimpleSchema)
135
+ const doc = createDoc(yjs.bind(SimpleSchema))
126
136
  change(doc, (d: any) => {
127
137
  d.count.set(42)
128
138
  })
@@ -130,7 +140,7 @@ describe("YjsSubstrate", () => {
130
140
  })
131
141
 
132
142
  it("list push round-trips through prepare/flush", () => {
133
- const doc = createYjsDoc(SimpleSchema)
143
+ const doc = createDoc(yjs.bind(SimpleSchema))
134
144
  change(doc, (d: any) => {
135
145
  d.items.push("a")
136
146
  })
@@ -148,7 +158,7 @@ describe("YjsSubstrate", () => {
148
158
 
149
159
  describe("version tracking", () => {
150
160
  it("version advances after mutations", () => {
151
- const doc = createYjsDoc(SimpleSchema)
161
+ const doc = createDoc(yjs.bind(SimpleSchema))
152
162
  const v1 = version(doc)
153
163
 
154
164
  change(doc, (d: any) => {
@@ -161,7 +171,7 @@ describe("YjsSubstrate", () => {
161
171
  })
162
172
 
163
173
  it("version serialize/parse round-trips", () => {
164
- const doc = createYjsDoc(SimpleSchema)
174
+ const doc = createDoc(yjs.bind(SimpleSchema))
165
175
  change(doc, (d: any) => {
166
176
  d.title.insert(0, "Test")
167
177
  d.count.set(5)
@@ -180,7 +190,7 @@ describe("YjsSubstrate", () => {
180
190
 
181
191
  describe("export/import snapshot", () => {
182
192
  it("exports a binary payload", () => {
183
- const doc = createYjsDoc(SimpleSchema)
193
+ const doc = createDoc(yjs.bind(SimpleSchema))
184
194
  change(doc, (d: any) => {
185
195
  d.title.insert(0, "Snapshot")
186
196
  })
@@ -190,7 +200,7 @@ describe("YjsSubstrate", () => {
190
200
  })
191
201
 
192
202
  it("reconstructs equivalent state from snapshot", () => {
193
- const doc1 = createYjsDoc(SimpleSchema)
203
+ const doc1 = createDoc(yjs.bind(SimpleSchema))
194
204
  change(doc1, (d: any) => {
195
205
  d.title.insert(0, "Hello")
196
206
  d.count.set(42)
@@ -203,7 +213,7 @@ describe("YjsSubstrate", () => {
203
213
  })
204
214
 
205
215
  const payload = exportEntirety(doc1)
206
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
216
+ const doc2 = createDoc(yjs.bind(SimpleSchema), payload)
207
217
 
208
218
  expect(doc2.title()).toBe("Hello World")
209
219
  expect(doc2.count()).toBe(42)
@@ -217,11 +227,11 @@ describe("YjsSubstrate", () => {
217
227
 
218
228
  describe("delta sync", () => {
219
229
  it("exportSince → merge syncs state", () => {
220
- const doc1 = createYjsDoc(SimpleSchema)
230
+ const doc1 = createDoc(yjs.bind(SimpleSchema))
221
231
  change(doc1, (d: any) => {
222
232
  d.title.insert(0, "Start")
223
233
  })
224
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
234
+ const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
225
235
 
226
236
  const v1Before = version(doc1)
227
237
 
@@ -239,8 +249,8 @@ describe("YjsSubstrate", () => {
239
249
  })
240
250
 
241
251
  it("concurrent sync — two substrates converge after bidirectional sync", () => {
242
- const doc1 = createYjsDoc(SimpleSchema)
243
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
252
+ const doc1 = createDoc(yjs.bind(SimpleSchema))
253
+ const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
244
254
 
245
255
  const v1Before = version(doc1)
246
256
  const v2Before = version(doc2)
@@ -285,11 +295,11 @@ describe("YjsSubstrate", () => {
285
295
 
286
296
  describe("changefeed", () => {
287
297
  it("fires on merge", () => {
288
- const doc1 = createYjsDoc(SimpleSchema)
298
+ const doc1 = createDoc(yjs.bind(SimpleSchema))
289
299
  change(doc1, (d: any) => {
290
300
  d.title.insert(0, "A")
291
301
  })
292
- const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
302
+ const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
293
303
 
294
304
  const v2Before = version(doc2)
295
305
 
@@ -312,7 +322,10 @@ describe("YjsSubstrate", () => {
312
322
  it("fires on external Y.Doc mutation (raw Yjs API)", () => {
313
323
  const yjsDoc = new Y.Doc()
314
324
  ensureContainers(yjsDoc, SimpleSchema)
315
- const doc = createYjsDoc(SimpleSchema, yjsDoc)
325
+ const doc = createRef(
326
+ SimpleSchema,
327
+ createYjsSubstrate(yjsDoc, SimpleSchema),
328
+ )
316
329
 
317
330
  const received: any[] = []
318
331
  subscribe(doc, (changeset: any) => {
@@ -328,7 +341,7 @@ describe("YjsSubstrate", () => {
328
341
  })
329
342
 
330
343
  it("no double-fire on kyneta local writes", () => {
331
- const doc = createYjsDoc(SimpleSchema)
344
+ const doc = createDoc(yjs.bind(SimpleSchema))
332
345
 
333
346
  const received: any[] = []
334
347
  subscribe(doc, (changeset: any) => {
@@ -345,18 +358,15 @@ describe("YjsSubstrate", () => {
345
358
  })
346
359
 
347
360
  it("nested struct field changefeed fires on merge", () => {
348
- const doc1 = createYjsDoc(StructListSchema)
349
- const _doc2 = createYjsDocFromEntirety(
350
- StructListSchema,
351
- exportEntirety(doc1),
352
- )
361
+ const doc1 = createDoc(yjs.bind(StructListSchema))
362
+ const _doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
353
363
 
354
364
  // Add a struct item on doc1, sync to doc2
355
365
  change(doc1, (d: any) => {
356
366
  d.tasks.push({ name: "Buy milk", done: false })
357
367
  })
358
368
  const snap = exportEntirety(doc1)
359
- const doc2b = createYjsDocFromEntirety(StructListSchema, snap)
369
+ const doc2b = createDoc(yjs.bind(StructListSchema), snap)
360
370
 
361
371
  const taskB = [...doc2b.tasks][0] as any
362
372
  expect(taskB.done()).toBe(false)
@@ -387,16 +397,13 @@ describe("YjsSubstrate", () => {
387
397
  })
388
398
 
389
399
  it("multi-key struct update fires per-field changefeeds on merge", () => {
390
- const doc1 = createYjsDoc(StructListSchema)
400
+ const doc1 = createDoc(yjs.bind(StructListSchema))
391
401
 
392
402
  // Add a struct item, sync to doc2
393
403
  change(doc1, (d: any) => {
394
404
  d.tasks.push({ name: "Buy milk", done: false })
395
405
  })
396
- const doc2 = createYjsDocFromEntirety(
397
- StructListSchema,
398
- exportEntirety(doc1),
399
- )
406
+ const doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
400
407
 
401
408
  const taskB = [...doc2.tasks][0] as any
402
409
  const v2 = version(doc2)
@@ -438,7 +445,7 @@ describe("YjsSubstrate", () => {
438
445
 
439
446
  describe("transaction support", () => {
440
447
  it("multi-op change() is atomic", () => {
441
- const doc = createYjsDoc(SimpleSchema)
448
+ const doc = createDoc(yjs.bind(SimpleSchema))
442
449
 
443
450
  const received: any[] = []
444
451
  subscribe(doc, (changeset: any) => {
@@ -474,7 +481,7 @@ describe("YjsSubstrate", () => {
474
481
 
475
482
  describe("nested structure", () => {
476
483
  it("push struct into list, read back via navigation", () => {
477
- const doc = createYjsDoc(StructListSchema)
484
+ const doc = createDoc(yjs.bind(StructListSchema))
478
485
 
479
486
  change(doc, (d: any) => {
480
487
  d.tasks.push({ name: "Task 1", done: false })
@@ -494,7 +501,7 @@ describe("YjsSubstrate", () => {
494
501
  })
495
502
 
496
503
  it("nested struct write round-trip", () => {
497
- const doc = createYjsDoc(FullSchema)
504
+ const doc = createDoc(yjs.bind(FullSchema))
498
505
  change(doc, (d: any) => {
499
506
  d.meta.author.set("Alice")
500
507
  })
@@ -553,7 +560,7 @@ describe("YjsSubstrate", () => {
553
560
  })
554
561
 
555
562
  it("reconstructs from snapshot with correct state", () => {
556
- const doc = createYjsDoc(SimpleSchema)
563
+ const doc = createDoc(yjs.bind(SimpleSchema))
557
564
  change(doc, (d: any) => {
558
565
  d.title.insert(0, "Snapshot Test")
559
566
  d.count.set(77)
@@ -561,7 +568,7 @@ describe("YjsSubstrate", () => {
561
568
  })
562
569
 
563
570
  const payload = exportEntirety(doc)
564
- const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
571
+ const doc2 = createDoc(yjs.bind(SimpleSchema), payload)
565
572
 
566
573
  expect(doc2.title()).toBe("Snapshot Test")
567
574
  expect(doc2.count()).toBe(77)
package/src/bind-yjs.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // bind-yjs — Yjs CRDT substrate namespace and factory.
2
2
  //
3
- // Provides the `yjs` substrate namespace (`yjs.bind()`, `yjs.replica()`,
4
- // `yjs.unwrap()`) and the internal factory builder that injects a
5
- // deterministic numeric Yjs clientID derived from the exchange's peerId.
3
+ // Provides the `yjs` substrate namespace (`yjs.bind()`, `yjs.replica()`)
4
+ // and the internal factory builder that injects a deterministic numeric
5
+ // Yjs clientID derived from the exchange's peerId.
6
6
  //
7
7
  // Yjs clientID is a uint32 number. We use FNV-1a hash truncated to
8
8
  // 32 bits, mirroring the Loro binding's hashPeerId pattern but
@@ -31,9 +31,9 @@ import {
31
31
  BACKING_DOC,
32
32
  createSubstrateNamespace,
33
33
  STRUCTURAL_YJS_CLIENT_ID,
34
- unwrap,
35
34
  } from "@kyneta/schema"
36
35
  import * as Y from "yjs"
36
+ import type { YjsNativeMap } from "./native-map.js"
37
37
  import { ensureContainers } from "./populate.js"
38
38
  import {
39
39
  createYjsReplica,
@@ -142,19 +142,17 @@ function createYjsFactory(peerId: string): SubstrateFactory<YjsVersion> {
142
142
  * - `yjs.bind(schema, "ephemeral")` — ephemeral/presence broadcast
143
143
  * - `yjs.replica()` — collaborative replication (default)
144
144
  * - `yjs.replica("ephemeral")` — ephemeral replication
145
- * - `yjs.unwrap(ref)` — access the underlying Y.Doc
146
145
  *
147
146
  * Strategy is constrained to `CrdtStrategy` (`"collaborative" | "ephemeral"`).
148
147
  * Passing `"authoritative"` is a compile error.
148
+ *
149
+ * To access the underlying Y.Doc, use `unwrap(ref)` from `@kyneta/schema`.
149
150
  */
150
151
  /** The closed set of capability tags that the Yjs substrate supports. */
151
152
  export type YjsCaps = "text" | "json"
152
153
 
153
- export const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps> & {
154
- /** Access the underlying `Y.Doc` backing a ref. */
155
- unwrap(ref: object): Y.Doc
156
- } = {
157
- ...createSubstrateNamespace<CrdtStrategy, YjsCaps>({
154
+ export const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps, YjsNativeMap> =
155
+ createSubstrateNamespace<CrdtStrategy, YjsCaps, YjsNativeMap>({
158
156
  strategies: {
159
157
  collaborative: {
160
158
  factory: ctx => createYjsFactory(ctx.peerId),
@@ -166,33 +164,4 @@ export const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps> & {
166
164
  },
167
165
  },
168
166
  defaultStrategy: "collaborative",
169
- }),
170
-
171
- unwrap(ref: object): Y.Doc {
172
- let substrate: any
173
- try {
174
- substrate = unwrap(ref)
175
- } catch {
176
- throw new Error(
177
- "yjs.unwrap() requires a ref backed by a Yjs substrate. " +
178
- "Use a doc created by exchange.get() with a yjs.bind() schema, " +
179
- "or by createYjsDoc().",
180
- )
181
- }
182
-
183
- const doc = substrate[BACKING_DOC]
184
- if (
185
- !doc ||
186
- typeof doc !== "object" ||
187
- typeof (doc as any).getMap !== "function" ||
188
- typeof (doc as any).clientID !== "number"
189
- ) {
190
- throw new Error(
191
- "yjs.unwrap() requires a ref backed by a Yjs substrate. " +
192
- "The ref has a substrate but it is not a Yjs substrate. " +
193
- "Use a doc created with a yjs.bind() schema or createYjsDoc().",
194
- )
195
- }
196
- return doc as Y.Doc
197
- },
198
- }
167
+ })
@@ -29,7 +29,12 @@ import type {
29
29
  TextChange,
30
30
  TextInstruction,
31
31
  } from "@kyneta/schema"
32
- import { advanceSchema, expandMapOpsToLeaves, KIND, RawPath } from "@kyneta/schema"
32
+ import {
33
+ advanceSchema,
34
+ expandMapOpsToLeaves,
35
+ KIND,
36
+ RawPath,
37
+ } from "@kyneta/schema"
33
38
  import * as Y from "yjs"
34
39
  import { resolveYjsType } from "./yjs-resolve.js"
35
40
 
@@ -585,4 +590,4 @@ function getFieldSchema(
585
590
 
586
591
  function pathToString(path: Path): string {
587
592
  return path.segments.map(seg => String(seg.resolve())).join(".")
588
- }
593
+ }