@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.
@@ -0,0 +1,325 @@
1
+ // bind-constraints — compile-time and runtime tests for yjs.bind() caps enforcement.
2
+ //
3
+ // Verifies that `yjs.bind()` rejects schemas containing capabilities
4
+ // that Yjs doesn't support (counter, movable, tree, set) at COMPILE TIME
5
+ // via the `RestrictCaps` / `AllowedCaps` mechanism, while accepting
6
+ // capabilities it does support (text) and plain schemas.
7
+
8
+ import {
9
+ type BoundSchema,
10
+ type ExtractCaps,
11
+ json,
12
+ Schema,
13
+ } from "@kyneta/schema"
14
+ import { describe, expect, expectTypeOf, it } from "vitest"
15
+ import { yjs } from "../bind-yjs.js"
16
+
17
+ // ===========================================================================
18
+ // §1 — Compile-time acceptance: schemas that yjs.bind() SHOULD accept
19
+ // ===========================================================================
20
+
21
+ describe("yjs.bind() accepts Yjs-compatible schemas", () => {
22
+ it("plain schema (no caps)", () => {
23
+ const schema = Schema.struct({
24
+ name: Schema.string(),
25
+ count: Schema.number(),
26
+ active: Schema.boolean(),
27
+ })
28
+ const bound = yjs.bind(schema)
29
+ expect(bound).toBeDefined()
30
+ expect(bound.schema).toBe(schema)
31
+ expectTypeOf(bound).toMatchTypeOf<BoundSchema<typeof schema>>()
32
+ })
33
+
34
+ it("schema with text", () => {
35
+ const schema = Schema.struct({
36
+ title: Schema.text(),
37
+ })
38
+ const bound = yjs.bind(schema)
39
+ expect(bound).toBeDefined()
40
+ expect(bound.schema).toBe(schema)
41
+ })
42
+
43
+ it("schema with text + plain scalars", () => {
44
+ const schema = Schema.struct({
45
+ title: Schema.text(),
46
+ count: Schema.number(),
47
+ active: Schema.boolean(),
48
+ tags: Schema.list(Schema.string()),
49
+ })
50
+ const bound = yjs.bind(schema)
51
+ expect(bound).toBeDefined()
52
+ })
53
+
54
+ it("deeply nested text is accepted", () => {
55
+ const schema = Schema.struct({
56
+ channels: Schema.list(
57
+ Schema.struct({
58
+ meta: Schema.record(
59
+ Schema.struct({
60
+ description: Schema.text(),
61
+ }),
62
+ ),
63
+ }),
64
+ ),
65
+ })
66
+ const bound = yjs.bind(schema)
67
+ expect(bound).toBeDefined()
68
+ })
69
+
70
+ it("optional text field alongside plain scalars is accepted", () => {
71
+ const schema = Schema.struct({
72
+ title: Schema.text(),
73
+ draft: Schema.text(),
74
+ count: Schema.number(),
75
+ })
76
+ const bound = yjs.bind(schema)
77
+ expect(bound).toBeDefined()
78
+ })
79
+
80
+ it("preserves full schema type through bind()", () => {
81
+ const schema = Schema.struct({
82
+ title: Schema.text(),
83
+ items: Schema.list(
84
+ Schema.struct({ name: Schema.string(), done: Schema.boolean() }),
85
+ ),
86
+ })
87
+ const bound = yjs.bind(schema)
88
+ expectTypeOf(bound.schema).toEqualTypeOf(schema)
89
+ })
90
+
91
+ it("json merge boundary is accepted", () => {
92
+ const schema = Schema.struct({
93
+ title: Schema.text(),
94
+ metadata: Schema.struct.json({
95
+ version: Schema.number(),
96
+ tags: Schema.list(Schema.string()),
97
+ }),
98
+ })
99
+ const bound = yjs.bind(schema)
100
+ expect(bound).toBeDefined()
101
+ })
102
+ })
103
+
104
+ // ===========================================================================
105
+ // §2 — Compile-time rejection: schemas that yjs.bind() SHOULD reject
106
+ // ===========================================================================
107
+
108
+ describe("yjs.bind() rejects schemas with unsupported caps", () => {
109
+ it("rejects counter", () => {
110
+ const schema = Schema.struct({
111
+ count: Schema.counter(),
112
+ })
113
+ // @ts-expect-error — counter is not in YjsCaps
114
+ yjs.bind(schema)
115
+ })
116
+
117
+ it("rejects movableList", () => {
118
+ const schema = Schema.struct({
119
+ items: Schema.movableList(Schema.string()),
120
+ })
121
+ // @ts-expect-error — movable is not in YjsCaps
122
+ yjs.bind(schema)
123
+ })
124
+
125
+ it("rejects tree", () => {
126
+ const schema = Schema.struct({
127
+ hierarchy: Schema.tree(Schema.struct({ label: Schema.string() })),
128
+ })
129
+ // @ts-expect-error — tree is not in YjsCaps
130
+ yjs.bind(schema)
131
+ })
132
+
133
+ it("rejects set", () => {
134
+ const schema = Schema.struct({
135
+ tags: Schema.set(Schema.string()),
136
+ })
137
+ // @ts-expect-error — set is not in YjsCaps
138
+ yjs.bind(schema)
139
+ })
140
+
141
+ it("rejects deeply nested counter", () => {
142
+ const schema = Schema.struct({
143
+ items: Schema.list(
144
+ Schema.struct({
145
+ meta: Schema.record(Schema.struct({ hits: Schema.counter() })),
146
+ }),
147
+ ),
148
+ })
149
+ // @ts-expect-error — counter is deeply nested but still caught
150
+ yjs.bind(schema)
151
+ })
152
+
153
+ it("rejects mix of supported and unsupported (text + counter)", () => {
154
+ const schema = Schema.struct({
155
+ title: Schema.text(),
156
+ views: Schema.counter(),
157
+ })
158
+ // @ts-expect-error — counter is not in YjsCaps
159
+ yjs.bind(schema)
160
+ })
161
+
162
+ it("rejects counter inside list", () => {
163
+ const schema = Schema.struct({
164
+ scores: Schema.list(Schema.struct({ value: Schema.counter() })),
165
+ })
166
+ // @ts-expect-error — counter nested inside list struct
167
+ yjs.bind(schema)
168
+ })
169
+ })
170
+
171
+ // ===========================================================================
172
+ // §3 — Cross-substrate: same schema, different bind targets
173
+ // ===========================================================================
174
+
175
+ describe("cross-substrate: universal schema vs substrate-specific schema", () => {
176
+ // A schema using only universally-supported features
177
+ const universalSchema = Schema.struct({
178
+ title: Schema.text(),
179
+ items: Schema.list(
180
+ Schema.struct({
181
+ name: Schema.string(),
182
+ done: Schema.boolean(),
183
+ }),
184
+ ),
185
+ })
186
+
187
+ // A schema using Loro-specific features (counter, movable)
188
+ const loroSpecificSchema = Schema.struct({
189
+ title: Schema.text(),
190
+ count: Schema.counter(),
191
+ tasks: Schema.movableList(Schema.struct({ name: Schema.string() })),
192
+ })
193
+
194
+ it("universal schema is Yjs-compatible (ExtractCaps check)", () => {
195
+ type Caps = ExtractCaps<typeof universalSchema>
196
+ // Only "text" — in YjsCaps
197
+ expectTypeOf<Caps>().toEqualTypeOf<"text">()
198
+ })
199
+
200
+ it("Loro-specific schema is NOT Yjs-compatible (ExtractCaps check)", () => {
201
+ type Caps = ExtractCaps<typeof loroSpecificSchema>
202
+ // Includes "counter" and "movable" which are NOT in YjsCaps
203
+ expectTypeOf<Caps>().toEqualTypeOf<"text" | "counter" | "movable">()
204
+ })
205
+
206
+ it("universal schema binds to yjs", () => {
207
+ const bound = yjs.bind(universalSchema)
208
+ expect(bound).toBeDefined()
209
+ expect(bound.schema).toBe(universalSchema)
210
+ })
211
+
212
+ it("Loro-specific schema is rejected by yjs.bind()", () => {
213
+ // @ts-expect-error — counter and movable not in YjsCaps
214
+ yjs.bind(loroSpecificSchema)
215
+ })
216
+
217
+ it("json.bind() accepts schemas with all caps (AllowedCaps = string)", () => {
218
+ const schema = Schema.struct({
219
+ title: Schema.text(),
220
+ count: Schema.counter(),
221
+ tasks: Schema.movableList(Schema.string()),
222
+ })
223
+ const bound = json.bind(schema)
224
+ expect(bound).toBeDefined()
225
+ expect(bound.schema).toBe(schema)
226
+ })
227
+ })
228
+
229
+ // ===========================================================================
230
+ // §4 — Edge cases: discriminated unions, multiple text fields
231
+ // ===========================================================================
232
+
233
+ describe("bind constraint edge cases", () => {
234
+ it("discriminated union with all-plain variants is accepted", () => {
235
+ const schema = Schema.struct({
236
+ content: Schema.discriminatedUnion("type", [
237
+ Schema.struct({
238
+ type: Schema.string("text"),
239
+ body: Schema.string(),
240
+ }),
241
+ Schema.struct({
242
+ type: Schema.string("image"),
243
+ url: Schema.string(),
244
+ }),
245
+ ]),
246
+ })
247
+ const bound = yjs.bind(schema)
248
+ expect(bound).toBeDefined()
249
+ })
250
+
251
+ it("struct with counter alongside plain variants is rejected", () => {
252
+ const schema = Schema.struct({
253
+ content: Schema.discriminatedUnion("type", [
254
+ Schema.struct({
255
+ type: Schema.string("text"),
256
+ body: Schema.string(),
257
+ }),
258
+ Schema.struct({
259
+ type: Schema.string("image"),
260
+ url: Schema.string(),
261
+ }),
262
+ ]),
263
+ hits: Schema.counter(),
264
+ })
265
+ // @ts-expect-error — counter taints the whole schema
266
+ yjs.bind(schema)
267
+ })
268
+
269
+ it("multiple text fields are all accepted", () => {
270
+ const schema = Schema.struct({
271
+ title: Schema.text(),
272
+ body: Schema.text(),
273
+ summary: Schema.text(),
274
+ })
275
+ const bound = yjs.bind(schema)
276
+ expect(bound).toBeDefined()
277
+ })
278
+
279
+ it("plain-only schema (no caps at all) is accepted", () => {
280
+ const schema = Schema.struct({
281
+ name: Schema.string(),
282
+ age: Schema.number(),
283
+ active: Schema.boolean(),
284
+ tags: Schema.list(Schema.string()),
285
+ address: Schema.struct({
286
+ street: Schema.string(),
287
+ city: Schema.string(),
288
+ }),
289
+ metadata: Schema.record(Schema.any()),
290
+ })
291
+ const bound = yjs.bind(schema)
292
+ expect(bound).toBeDefined()
293
+ })
294
+ })
295
+
296
+ // ===========================================================================
297
+ // §5 — Root kind rejection: bind() requires a product (struct) root
298
+ // ===========================================================================
299
+
300
+ describe("yjs.bind() rejects non-product root schemas", () => {
301
+ it("rejects bare list at root", () => {
302
+ // @ts-expect-error — SequenceSchema is not ProductSchema
303
+ yjs.bind(Schema.list(Schema.string()))
304
+ })
305
+
306
+ it("rejects bare record at root", () => {
307
+ // @ts-expect-error — MapSchema is not ProductSchema
308
+ yjs.bind(Schema.record(Schema.string()))
309
+ })
310
+
311
+ it("rejects bare text at root", () => {
312
+ // @ts-expect-error — TextSchema is not ProductSchema
313
+ yjs.bind(Schema.text())
314
+ })
315
+
316
+ it("rejects bare scalar at root", () => {
317
+ // @ts-expect-error — ScalarSchema is not ProductSchema
318
+ yjs.bind(Schema.string())
319
+ })
320
+
321
+ it("rejects list of structs at root", () => {
322
+ // @ts-expect-error — SequenceSchema<ProductSchema> is still not ProductSchema
323
+ yjs.bind(Schema.list(Schema.struct({ name: Schema.string() })))
324
+ })
325
+ })
@@ -1,17 +1,27 @@
1
- import { change, RawPath, Schema } from "@kyneta/schema"
1
+ import {
2
+ change,
3
+ createRef,
4
+ exportEntirety,
5
+ RawPath,
6
+ Schema,
7
+ unwrap,
8
+ } from "@kyneta/schema"
2
9
  import { describe, expect, it } from "vitest"
3
10
  import * as Y from "yjs"
4
- import { bindYjs } from "../bind-yjs.js"
5
- import { createYjsDoc } from "../create.js"
6
- import { yjsSubstrateFactory } from "../substrate.js"
7
- import { yjs } from "../yjs-escape.js"
11
+ import { yjs } from "../bind-yjs.js"
12
+
13
+ // ===========================================================================
14
+ // Helper createDoc using the generic API
15
+ // ===========================================================================
16
+
17
+ import { createDoc } from "@kyneta/schema"
8
18
 
9
19
  // ===========================================================================
10
20
  // Schemas used across tests
11
21
  // ===========================================================================
12
22
 
13
- const TodoSchema = Schema.doc({
14
- title: Schema.annotated("text"),
23
+ const TodoSchema = Schema.struct({
24
+ title: Schema.text(),
15
25
  items: Schema.list(
16
26
  Schema.struct({
17
27
  name: Schema.string(),
@@ -20,8 +30,8 @@ const TodoSchema = Schema.doc({
20
30
  ),
21
31
  })
22
32
 
23
- const SimpleSchema = Schema.doc({
24
- title: Schema.annotated("text"),
33
+ const SimpleSchema = Schema.struct({
34
+ title: Schema.text(),
25
35
  count: Schema.number(),
26
36
  })
27
37
 
@@ -29,26 +39,26 @@ const SimpleSchema = Schema.doc({
29
39
  // Tests
30
40
  // ===========================================================================
31
41
 
32
- describe("bindYjs", () => {
42
+ describe("yjs.bind", () => {
33
43
  // -------------------------------------------------------------------------
34
44
  // BoundSchema shape
35
45
  // -------------------------------------------------------------------------
36
46
 
37
47
  describe("BoundSchema", () => {
38
- it("creates BoundSchema with causal strategy", () => {
39
- const bound = bindYjs(TodoSchema)
48
+ it("creates BoundSchema with collaborative strategy", () => {
49
+ const bound = yjs.bind(TodoSchema)
40
50
  expect(bound._brand).toBe("BoundSchema")
41
51
  expect(bound.schema).toBe(TodoSchema)
42
- expect(bound.strategy).toBe("causal")
52
+ expect(bound.strategy).toBe("collaborative")
43
53
  })
44
54
 
45
55
  it("has a factory builder function", () => {
46
- const bound = bindYjs(TodoSchema)
56
+ const bound = yjs.bind(TodoSchema)
47
57
  expect(typeof bound.factory).toBe("function")
48
58
  })
49
59
 
50
60
  it("preserves the schema reference", () => {
51
- const bound = bindYjs(SimpleSchema)
61
+ const bound = yjs.bind(SimpleSchema)
52
62
  expect(bound.schema).toBe(SimpleSchema)
53
63
  })
54
64
  })
@@ -59,10 +69,10 @@ describe("bindYjs", () => {
59
69
 
60
70
  describe("factory builder", () => {
61
71
  it("produces a working SubstrateFactory", () => {
62
- const bound = bindYjs(SimpleSchema)
72
+ const bound = yjs.bind(SimpleSchema)
63
73
  const factory = bound.factory({ peerId: "peer-1" })
64
74
 
65
- const substrate = factory.create(SimpleSchema)
75
+ const _substrate = factory.create(SimpleSchema)
66
76
 
67
77
  // Populate via the substrate's writable context
68
78
  const doc = createYjsDocFromFactory(factory, SimpleSchema)
@@ -76,7 +86,7 @@ describe("bindYjs", () => {
76
86
  })
77
87
 
78
88
  it("factory supports fromEntirety", () => {
79
- const bound = bindYjs(SimpleSchema)
89
+ const bound = yjs.bind(SimpleSchema)
80
90
  const factory = bound.factory({ peerId: "peer-1" })
81
91
 
82
92
  // Create and populate
@@ -85,8 +95,7 @@ describe("bindYjs", () => {
85
95
  d.title.insert(0, "Snap")
86
96
  d.count.set(42)
87
97
  })
88
- const substrate1 = (unwrap as any)(doc1)
89
- const snapshot = substrate1.exportEntirety()
98
+ const snapshot = exportEntirety(doc1)
90
99
 
91
100
  // Restore
92
101
  const substrate2 = factory.fromEntirety(snapshot, SimpleSchema)
@@ -95,7 +104,7 @@ describe("bindYjs", () => {
95
104
  })
96
105
 
97
106
  it("factory supports parseVersion", () => {
98
- const bound = bindYjs(SimpleSchema)
107
+ const bound = yjs.bind(SimpleSchema)
99
108
  const factory = bound.factory({ peerId: "peer-1" })
100
109
 
101
110
  const substrate = factory.create(SimpleSchema)
@@ -112,34 +121,44 @@ describe("bindYjs", () => {
112
121
 
113
122
  describe("deterministic clientID", () => {
114
123
  it("same peerId produces same clientID across multiple factory calls", () => {
115
- const bound = bindYjs(SimpleSchema)
124
+ const bound = yjs.bind(SimpleSchema)
116
125
  const factory = bound.factory({ peerId: "stable-peer-id" })
117
126
 
118
- const s1 = factory.create(SimpleSchema)
119
- const s2 = factory.create(SimpleSchema)
127
+ const _s1 = factory.create(SimpleSchema)
128
+ const _s2 = factory.create(SimpleSchema)
120
129
 
121
130
  // Both docs should have the same clientID
122
- const doc1 = yjs(createYjsDocFromFactory(factory, SimpleSchema))
123
- const doc2 = yjs(createYjsDocFromFactory(factory, SimpleSchema))
131
+ const doc1 = unwrap(
132
+ createYjsDocFromFactory(factory, SimpleSchema),
133
+ ) as Y.Doc
134
+ const doc2 = unwrap(
135
+ createYjsDocFromFactory(factory, SimpleSchema),
136
+ ) as Y.Doc
124
137
 
125
138
  expect(doc1.clientID).toBe(doc2.clientID)
126
139
  })
127
140
 
128
141
  it("different peerIds produce different clientIDs", () => {
129
- const bound = bindYjs(SimpleSchema)
142
+ const bound = yjs.bind(SimpleSchema)
130
143
  const factory1 = bound.factory({ peerId: "peer-alpha" })
131
144
  const factory2 = bound.factory({ peerId: "peer-beta" })
132
145
 
133
- const doc1 = yjs(createYjsDocFromFactory(factory1, SimpleSchema))
134
- const doc2 = yjs(createYjsDocFromFactory(factory2, SimpleSchema))
146
+ const doc1 = unwrap(
147
+ createYjsDocFromFactory(factory1, SimpleSchema),
148
+ ) as Y.Doc
149
+ const doc2 = unwrap(
150
+ createYjsDocFromFactory(factory2, SimpleSchema),
151
+ ) as Y.Doc
135
152
 
136
153
  expect(doc1.clientID).not.toBe(doc2.clientID)
137
154
  })
138
155
 
139
156
  it("clientID is a valid uint32", () => {
140
- const bound = bindYjs(SimpleSchema)
157
+ const bound = yjs.bind(SimpleSchema)
141
158
  const factory = bound.factory({ peerId: "test-peer-id-12345" })
142
- const doc = yjs(createYjsDocFromFactory(factory, SimpleSchema))
159
+ const doc = unwrap(
160
+ createYjsDocFromFactory(factory, SimpleSchema),
161
+ ) as Y.Doc
143
162
 
144
163
  expect(typeof doc.clientID).toBe("number")
145
164
  expect(doc.clientID).toBeGreaterThanOrEqual(0)
@@ -151,63 +170,72 @@ describe("bindYjs", () => {
151
170
  const peerId = "deterministic-check-peer"
152
171
 
153
172
  // Simulate two separate "sessions" — both should hash to the same value
154
- const bound1 = bindYjs(SimpleSchema)
173
+ const bound1 = yjs.bind(SimpleSchema)
155
174
  const factory1 = bound1.factory({ peerId })
156
- const doc1 = yjs(createYjsDocFromFactory(factory1, SimpleSchema))
175
+ const doc1 = unwrap(
176
+ createYjsDocFromFactory(factory1, SimpleSchema),
177
+ ) as Y.Doc
157
178
 
158
- const bound2 = bindYjs(SimpleSchema)
179
+ const bound2 = yjs.bind(SimpleSchema)
159
180
  const factory2 = bound2.factory({ peerId })
160
- const doc2 = yjs(createYjsDocFromFactory(factory2, SimpleSchema))
181
+ const doc2 = unwrap(
182
+ createYjsDocFromFactory(factory2, SimpleSchema),
183
+ ) as Y.Doc
161
184
 
162
185
  expect(doc1.clientID).toBe(doc2.clientID)
163
186
  })
164
187
  })
165
188
 
166
189
  // -------------------------------------------------------------------------
167
- // yjs() escape hatch
190
+ // unwrap() escape hatch
168
191
  // -------------------------------------------------------------------------
169
192
 
170
- describe("yjs() escape hatch", () => {
171
- it("returns the underlying Y.Doc from a createYjsDoc ref", () => {
172
- const doc = createYjsDoc(SimpleSchema)
193
+ describe("unwrap() escape hatch", () => {
194
+ it("returns the underlying Y.Doc from a createDoc ref", () => {
195
+ const doc = createDoc(yjs.bind(SimpleSchema))
173
196
  change(doc, (d: any) => {
174
197
  d.title.insert(0, "Escape")
175
198
  d.count.set(0)
176
199
  })
177
- const yjsDoc = yjs(doc)
200
+ const yjsDoc = unwrap(doc) as Y.Doc
178
201
 
179
202
  expect(yjsDoc).toBeInstanceOf(Y.Doc)
180
203
  expect(yjsDoc.getMap("root").get("count")).toBe(0)
181
204
  })
182
205
 
183
206
  it("returns a Y.Doc with the correct root map state", () => {
184
- const doc = createYjsDoc(SimpleSchema)
207
+ const doc = createDoc(yjs.bind(SimpleSchema))
185
208
  change(doc, (d: any) => {
186
209
  d.title.insert(0, "Hello")
187
210
  d.count.set(42)
188
211
  })
189
- const yjsDoc = yjs(doc)
212
+ const yjsDoc = unwrap(doc) as Y.Doc
190
213
  const rootMap = yjsDoc.getMap("root")
191
214
 
192
215
  expect((rootMap.get("title") as Y.Text).toJSON()).toBe("Hello")
193
216
  expect(rootMap.get("count")).toBe(42)
194
217
  })
195
218
 
196
- it("throws for non-Yjs refs (plain object)", () => {
197
- expect(() => yjs({})).toThrow("yjs() requires a ref")
219
+ it("returns undefined for non-refs (plain object)", () => {
220
+ expect(unwrap({} as any)).toBeUndefined()
198
221
  })
199
222
 
200
- it("throws for non-Yjs refs (random object with properties)", () => {
223
+ it("returns undefined for non-refs (random object with properties)", () => {
201
224
  const fake = {
202
225
  title: () => "fake",
203
226
  count: () => 0,
204
227
  }
205
- expect(() => yjs(fake)).toThrow("yjs() requires a ref")
228
+ expect(unwrap(fake as any)).toBeUndefined()
229
+ })
230
+
231
+ it("throws for primitives", () => {
232
+ expect(() => unwrap(null as any)).toThrow("unwrap() requires a ref")
233
+ expect(() => unwrap(undefined as any)).toThrow("unwrap() requires a ref")
206
234
  })
207
235
 
208
236
  it("mutations through escape hatch are visible via kyneta ref", () => {
209
- const doc = createYjsDoc(SimpleSchema)
210
- const yjsDoc = yjs(doc)
237
+ const doc = createDoc(yjs.bind(SimpleSchema))
238
+ const yjsDoc = unwrap(doc) as Y.Doc
211
239
 
212
240
  // Mutate via raw Yjs
213
241
  yjsDoc.getMap("root").set("count", 99)
@@ -215,11 +243,11 @@ describe("bindYjs", () => {
215
243
  })
216
244
 
217
245
  it("text mutations through escape hatch are visible", () => {
218
- const doc = createYjsDoc(SimpleSchema)
246
+ const doc = createDoc(yjs.bind(SimpleSchema))
219
247
  change(doc, (d: any) => {
220
248
  d.title.insert(0, "Hello")
221
249
  })
222
- const yjsDoc = yjs(doc)
250
+ const yjsDoc = unwrap(doc) as Y.Doc
223
251
 
224
252
  const text = yjsDoc.getMap("root").get("title") as Y.Text
225
253
  text.insert(5, " World")
@@ -233,14 +261,6 @@ describe("bindYjs", () => {
233
261
  // ===========================================================================
234
262
 
235
263
  import type { Schema as SchemaType, SubstrateFactory } from "@kyneta/schema"
236
- import {
237
- changefeed,
238
- interpret,
239
- readable,
240
- registerSubstrate,
241
- unwrap,
242
- writable,
243
- } from "@kyneta/schema"
244
264
 
245
265
  /**
246
266
  * Helper to create a kyneta ref from a factory (mimicking what exchange.get does).
@@ -251,16 +271,5 @@ function createYjsDocFromFactory(
251
271
  schema: SchemaType,
252
272
  ): any {
253
273
  const substrate = factory.create(schema)
254
- const doc: any = (interpret as any)(schema, substrate.context())
255
- .with(readable)
256
- .with(writable)
257
- .with(changefeed)
258
- .done()
259
-
260
- // Register for escape hatch — createYjsSubstrate already registered
261
- // substrate → Y.Doc internally, but we also need ref → substrate
262
- // for unwrap() (used by the yjs() escape hatch).
263
- registerSubstrate(doc, substrate)
264
-
265
- return doc
274
+ return createRef(schema, substrate)
266
275
  }