@kyneta/yjs-schema 1.0.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 +109 -147
- package/dist/index.js +321 -210
- 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 +53 -55
- package/src/__tests__/create.test.ts +71 -62
- package/src/__tests__/{store-reader.test.ts → reader.test.ts} +64 -90
- package/src/__tests__/record-text-spike.test.ts +38 -31
- package/src/__tests__/structural-merge.test.ts +362 -0
- package/src/__tests__/substrate.test.ts +65 -84
- package/src/__tests__/version.test.ts +82 -16
- package/src/bind-yjs.ts +115 -64
- package/src/change-mapping.ts +60 -84
- package/src/create.ts +33 -28
- package/src/index.ts +32 -51
- package/src/populate.ts +87 -92
- package/src/{store-reader.ts → reader.ts} +7 -12
- package/src/substrate.ts +186 -42
- package/src/sync.ts +26 -26
- package/src/version.ts +57 -4
- package/src/yjs-resolve.ts +5 -21
- package/src/yjs-escape.ts +0 -100
|
@@ -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
|
+
import { change, RawPath, Schema } from "@kyneta/schema"
|
|
1
2
|
import { describe, expect, it } from "vitest"
|
|
2
3
|
import * as Y from "yjs"
|
|
3
|
-
import {
|
|
4
|
-
import { bindYjs } from "../bind-yjs.js"
|
|
5
|
-
import { yjs } from "../yjs-escape.js"
|
|
4
|
+
import { yjs } from "../bind-yjs.js"
|
|
6
5
|
import { createYjsDoc } from "../create.js"
|
|
7
|
-
import { yjsSubstrateFactory } from "../substrate.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)
|
|
@@ -75,8 +73,8 @@ describe("bindYjs", () => {
|
|
|
75
73
|
expect(doc.count()).toBe(7)
|
|
76
74
|
})
|
|
77
75
|
|
|
78
|
-
it("factory supports
|
|
79
|
-
const bound =
|
|
76
|
+
it("factory supports fromEntirety", () => {
|
|
77
|
+
const bound = yjs.bind(SimpleSchema)
|
|
80
78
|
const factory = bound.factory({ peerId: "peer-1" })
|
|
81
79
|
|
|
82
80
|
// Create and populate
|
|
@@ -86,18 +84,16 @@ describe("bindYjs", () => {
|
|
|
86
84
|
d.count.set(42)
|
|
87
85
|
})
|
|
88
86
|
const substrate1 = (unwrap as any)(doc1)
|
|
89
|
-
const snapshot = substrate1.
|
|
87
|
+
const snapshot = substrate1.exportEntirety()
|
|
90
88
|
|
|
91
89
|
// Restore
|
|
92
|
-
const substrate2 = factory.
|
|
93
|
-
expect(substrate2.
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
expect(substrate2.store.read(RawPath.empty.field("count"))).toBe(42)
|
|
90
|
+
const substrate2 = factory.fromEntirety(snapshot, SimpleSchema)
|
|
91
|
+
expect(substrate2.reader.read(RawPath.empty.field("title"))).toBe("Snap")
|
|
92
|
+
expect(substrate2.reader.read(RawPath.empty.field("count"))).toBe(42)
|
|
97
93
|
})
|
|
98
94
|
|
|
99
95
|
it("factory supports parseVersion", () => {
|
|
100
|
-
const bound =
|
|
96
|
+
const bound = yjs.bind(SimpleSchema)
|
|
101
97
|
const factory = bound.factory({ peerId: "peer-1" })
|
|
102
98
|
|
|
103
99
|
const substrate = factory.create(SimpleSchema)
|
|
@@ -114,38 +110,34 @@ describe("bindYjs", () => {
|
|
|
114
110
|
|
|
115
111
|
describe("deterministic clientID", () => {
|
|
116
112
|
it("same peerId produces same clientID across multiple factory calls", () => {
|
|
117
|
-
const bound =
|
|
113
|
+
const bound = yjs.bind(SimpleSchema)
|
|
118
114
|
const factory = bound.factory({ peerId: "stable-peer-id" })
|
|
119
115
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
116
|
+
const _s1 = factory.create(SimpleSchema)
|
|
117
|
+
const _s2 = factory.create(SimpleSchema)
|
|
122
118
|
|
|
123
119
|
// Both docs should have the same clientID
|
|
124
|
-
const doc1 = yjs(
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
const doc2 = yjs(
|
|
128
|
-
createYjsDocFromFactory(factory, SimpleSchema),
|
|
129
|
-
)
|
|
120
|
+
const doc1 = yjs.unwrap(createYjsDocFromFactory(factory, SimpleSchema))
|
|
121
|
+
const doc2 = yjs.unwrap(createYjsDocFromFactory(factory, SimpleSchema))
|
|
130
122
|
|
|
131
123
|
expect(doc1.clientID).toBe(doc2.clientID)
|
|
132
124
|
})
|
|
133
125
|
|
|
134
126
|
it("different peerIds produce different clientIDs", () => {
|
|
135
|
-
const bound =
|
|
127
|
+
const bound = yjs.bind(SimpleSchema)
|
|
136
128
|
const factory1 = bound.factory({ peerId: "peer-alpha" })
|
|
137
129
|
const factory2 = bound.factory({ peerId: "peer-beta" })
|
|
138
130
|
|
|
139
|
-
const doc1 = yjs(createYjsDocFromFactory(factory1, SimpleSchema))
|
|
140
|
-
const doc2 = yjs(createYjsDocFromFactory(factory2, SimpleSchema))
|
|
131
|
+
const doc1 = yjs.unwrap(createYjsDocFromFactory(factory1, SimpleSchema))
|
|
132
|
+
const doc2 = yjs.unwrap(createYjsDocFromFactory(factory2, SimpleSchema))
|
|
141
133
|
|
|
142
134
|
expect(doc1.clientID).not.toBe(doc2.clientID)
|
|
143
135
|
})
|
|
144
136
|
|
|
145
137
|
it("clientID is a valid uint32", () => {
|
|
146
|
-
const bound =
|
|
138
|
+
const bound = yjs.bind(SimpleSchema)
|
|
147
139
|
const factory = bound.factory({ peerId: "test-peer-id-12345" })
|
|
148
|
-
const doc = yjs(createYjsDocFromFactory(factory, SimpleSchema))
|
|
140
|
+
const doc = yjs.unwrap(createYjsDocFromFactory(factory, SimpleSchema))
|
|
149
141
|
|
|
150
142
|
expect(typeof doc.clientID).toBe("number")
|
|
151
143
|
expect(doc.clientID).toBeGreaterThanOrEqual(0)
|
|
@@ -157,13 +149,13 @@ describe("bindYjs", () => {
|
|
|
157
149
|
const peerId = "deterministic-check-peer"
|
|
158
150
|
|
|
159
151
|
// Simulate two separate "sessions" — both should hash to the same value
|
|
160
|
-
const bound1 =
|
|
152
|
+
const bound1 = yjs.bind(SimpleSchema)
|
|
161
153
|
const factory1 = bound1.factory({ peerId })
|
|
162
|
-
const doc1 = yjs(createYjsDocFromFactory(factory1, SimpleSchema))
|
|
154
|
+
const doc1 = yjs.unwrap(createYjsDocFromFactory(factory1, SimpleSchema))
|
|
163
155
|
|
|
164
|
-
const bound2 =
|
|
156
|
+
const bound2 = yjs.bind(SimpleSchema)
|
|
165
157
|
const factory2 = bound2.factory({ peerId })
|
|
166
|
-
const doc2 = yjs(createYjsDocFromFactory(factory2, SimpleSchema))
|
|
158
|
+
const doc2 = yjs.unwrap(createYjsDocFromFactory(factory2, SimpleSchema))
|
|
167
159
|
|
|
168
160
|
expect(doc1.clientID).toBe(doc2.clientID)
|
|
169
161
|
})
|
|
@@ -173,14 +165,14 @@ describe("bindYjs", () => {
|
|
|
173
165
|
// yjs() escape hatch
|
|
174
166
|
// -------------------------------------------------------------------------
|
|
175
167
|
|
|
176
|
-
describe("yjs() escape hatch", () => {
|
|
168
|
+
describe("yjs.unwrap() escape hatch", () => {
|
|
177
169
|
it("returns the underlying Y.Doc from a createYjsDoc ref", () => {
|
|
178
170
|
const doc = createYjsDoc(SimpleSchema)
|
|
179
171
|
change(doc, (d: any) => {
|
|
180
172
|
d.title.insert(0, "Escape")
|
|
181
173
|
d.count.set(0)
|
|
182
174
|
})
|
|
183
|
-
const yjsDoc = yjs(doc)
|
|
175
|
+
const yjsDoc = yjs.unwrap(doc)
|
|
184
176
|
|
|
185
177
|
expect(yjsDoc).toBeInstanceOf(Y.Doc)
|
|
186
178
|
expect(yjsDoc.getMap("root").get("count")).toBe(0)
|
|
@@ -192,7 +184,7 @@ describe("bindYjs", () => {
|
|
|
192
184
|
d.title.insert(0, "Hello")
|
|
193
185
|
d.count.set(42)
|
|
194
186
|
})
|
|
195
|
-
const yjsDoc = yjs(doc)
|
|
187
|
+
const yjsDoc = yjs.unwrap(doc)
|
|
196
188
|
const rootMap = yjsDoc.getMap("root")
|
|
197
189
|
|
|
198
190
|
expect((rootMap.get("title") as Y.Text).toJSON()).toBe("Hello")
|
|
@@ -200,7 +192,7 @@ describe("bindYjs", () => {
|
|
|
200
192
|
})
|
|
201
193
|
|
|
202
194
|
it("throws for non-Yjs refs (plain object)", () => {
|
|
203
|
-
expect(() => yjs({})).toThrow("yjs() requires a ref")
|
|
195
|
+
expect(() => yjs.unwrap({})).toThrow("yjs.unwrap() requires a ref")
|
|
204
196
|
})
|
|
205
197
|
|
|
206
198
|
it("throws for non-Yjs refs (random object with properties)", () => {
|
|
@@ -208,12 +200,12 @@ describe("bindYjs", () => {
|
|
|
208
200
|
title: () => "fake",
|
|
209
201
|
count: () => 0,
|
|
210
202
|
}
|
|
211
|
-
expect(() => yjs(fake)).toThrow("yjs() requires a ref")
|
|
203
|
+
expect(() => yjs.unwrap(fake)).toThrow("yjs.unwrap() requires a ref")
|
|
212
204
|
})
|
|
213
205
|
|
|
214
206
|
it("mutations through escape hatch are visible via kyneta ref", () => {
|
|
215
207
|
const doc = createYjsDoc(SimpleSchema)
|
|
216
|
-
const yjsDoc = yjs(doc)
|
|
208
|
+
const yjsDoc = yjs.unwrap(doc)
|
|
217
209
|
|
|
218
210
|
// Mutate via raw Yjs
|
|
219
211
|
yjsDoc.getMap("root").set("count", 99)
|
|
@@ -225,7 +217,7 @@ describe("bindYjs", () => {
|
|
|
225
217
|
change(doc, (d: any) => {
|
|
226
218
|
d.title.insert(0, "Hello")
|
|
227
219
|
})
|
|
228
|
-
const yjsDoc = yjs(doc)
|
|
220
|
+
const yjsDoc = yjs.unwrap(doc)
|
|
229
221
|
|
|
230
222
|
const text = yjsDoc.getMap("root").get("title") as Y.Text
|
|
231
223
|
text.insert(5, " World")
|
|
@@ -238,9 +230,15 @@ describe("bindYjs", () => {
|
|
|
238
230
|
// Helper — create a doc via a factory and return a ref with escape hatch
|
|
239
231
|
// ===========================================================================
|
|
240
232
|
|
|
241
|
-
import {
|
|
242
|
-
import
|
|
243
|
-
|
|
233
|
+
import type { Schema as SchemaType, SubstrateFactory } from "@kyneta/schema"
|
|
234
|
+
import {
|
|
235
|
+
interpret,
|
|
236
|
+
observation,
|
|
237
|
+
readable,
|
|
238
|
+
registerSubstrate,
|
|
239
|
+
unwrap,
|
|
240
|
+
writable,
|
|
241
|
+
} from "@kyneta/schema"
|
|
244
242
|
|
|
245
243
|
/**
|
|
246
244
|
* Helper to create a kyneta ref from a factory (mimicking what exchange.get does).
|
|
@@ -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
|
|
@@ -263,4 +261,4 @@ function createYjsDocFromFactory(
|
|
|
263
261
|
registerSubstrate(doc, substrate)
|
|
264
262
|
|
|
265
263
|
return doc
|
|
266
|
-
}
|
|
264
|
+
}
|