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