@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.
- package/README.md +3 -3
- package/dist/index.d.ts +97 -225
- package/dist/index.js +281 -316
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +325 -0
- package/src/__tests__/bind-yjs.test.ts +79 -70
- package/src/__tests__/create.test.ts +88 -65
- package/src/__tests__/reader.test.ts +38 -72
- package/src/__tests__/record-text-spike.test.ts +47 -46
- package/src/__tests__/structural-merge.test.ts +18 -18
- package/src/__tests__/substrate.test.ts +62 -58
- package/src/__tests__/version.test.ts +75 -0
- package/src/bind-yjs.ts +40 -41
- package/src/change-mapping.ts +50 -54
- package/src/index.ts +29 -44
- package/src/native-map.ts +37 -0
- package/src/populate.ts +49 -82
- package/src/substrate.ts +68 -8
- package/src/version.ts +54 -52
- package/src/yjs-resolve.ts +0 -10
- package/src/create.ts +0 -177
- package/src/sync.ts +0 -107
- package/src/yjs-escape.ts +0 -84
package/src/change-mapping.ts
CHANGED
|
@@ -29,7 +29,12 @@ import type {
|
|
|
29
29
|
TextChange,
|
|
30
30
|
TextInstruction,
|
|
31
31
|
} from "@kyneta/schema"
|
|
32
|
-
import {
|
|
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
|
|
|
@@ -74,15 +79,15 @@ export function applyChangeToYjs(
|
|
|
74
79
|
|
|
75
80
|
case "increment":
|
|
76
81
|
throw new Error(
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
`Yjs substrate does not support "${change.type}" changes. ` +
|
|
83
|
+
`Counter requires a CRDT backend that supports counters (e.g. Loro). ` +
|
|
79
84
|
`Attempted IncrementChange with amount=${(change as IncrementChange).amount} at path [${pathToString(path)}].`,
|
|
80
85
|
)
|
|
81
86
|
|
|
82
87
|
case "tree":
|
|
83
88
|
throw new Error(
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
`Yjs substrate does not support "${change.type}" changes. ` +
|
|
90
|
+
`Tree requires a CRDT backend that supports trees (e.g. Loro). ` +
|
|
86
91
|
`Attempted TreeChange at path [${pathToString(path)}].`,
|
|
87
92
|
)
|
|
88
93
|
|
|
@@ -250,24 +255,16 @@ function maybeCreateSharedType(
|
|
|
250
255
|
): unknown {
|
|
251
256
|
if (schema === undefined) return value
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
text
|
|
258
|
+
switch (schema[KIND]) {
|
|
259
|
+
// First-class text → Y.Text
|
|
260
|
+
case "text": {
|
|
261
|
+
const text = new Y.Text()
|
|
262
|
+
if (typeof value === "string" && value.length > 0) {
|
|
263
|
+
text.insert(0, value)
|
|
264
|
+
}
|
|
265
|
+
return text
|
|
261
266
|
}
|
|
262
|
-
return text
|
|
263
|
-
}
|
|
264
267
|
|
|
265
|
-
// Annotated counter/movable/tree → should not reach here (thrown earlier)
|
|
266
|
-
if (tag === "counter" || tag === "movable" || tag === "tree") {
|
|
267
|
-
throw new Error(`Yjs substrate does not support "${tag}" annotations.`)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
switch (structural._kind) {
|
|
271
268
|
case "product": {
|
|
272
269
|
if (
|
|
273
270
|
value === null ||
|
|
@@ -277,13 +274,13 @@ function maybeCreateSharedType(
|
|
|
277
274
|
) {
|
|
278
275
|
return value
|
|
279
276
|
}
|
|
280
|
-
return createStructuredMap(value as Record<string, unknown>,
|
|
277
|
+
return createStructuredMap(value as Record<string, unknown>, schema)
|
|
281
278
|
}
|
|
282
279
|
|
|
283
280
|
case "sequence": {
|
|
284
281
|
if (!Array.isArray(value)) return value
|
|
285
282
|
const arr = new Y.Array()
|
|
286
|
-
const itemSchema =
|
|
283
|
+
const itemSchema = schema.item
|
|
287
284
|
const items = (value as unknown[]).map(item =>
|
|
288
285
|
maybeCreateSharedType(item, itemSchema),
|
|
289
286
|
)
|
|
@@ -301,15 +298,26 @@ function maybeCreateSharedType(
|
|
|
301
298
|
return value
|
|
302
299
|
}
|
|
303
300
|
const map = new Y.Map()
|
|
304
|
-
const valueSchema =
|
|
301
|
+
const valueSchema = schema.item
|
|
305
302
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
306
303
|
map.set(k, maybeCreateSharedType(v, valueSchema))
|
|
307
304
|
}
|
|
308
305
|
return map
|
|
309
306
|
}
|
|
310
307
|
|
|
308
|
+
// Unsupported first-class CRDT types — should not reach here
|
|
309
|
+
// (rejected at bind time by caps check)
|
|
310
|
+
case "counter":
|
|
311
|
+
case "set":
|
|
312
|
+
case "tree":
|
|
313
|
+
case "movable":
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Yjs substrate does not support [KIND]="${schema[KIND]}". ` +
|
|
316
|
+
`This should have been caught at bind() time.`,
|
|
317
|
+
)
|
|
318
|
+
|
|
311
319
|
default:
|
|
312
|
-
// Scalar, sum
|
|
320
|
+
// Scalar, sum — return as plain value
|
|
313
321
|
return value
|
|
314
322
|
}
|
|
315
323
|
}
|
|
@@ -326,9 +334,8 @@ function createStructuredMap(
|
|
|
326
334
|
productSchema: SchemaNode,
|
|
327
335
|
): Y.Map<any> {
|
|
328
336
|
const map = new Y.Map()
|
|
329
|
-
const structural = unwrapAnnotations(productSchema)
|
|
330
337
|
|
|
331
|
-
if (
|
|
338
|
+
if (productSchema[KIND] !== "product") {
|
|
332
339
|
// Fallback: set all values as plain
|
|
333
340
|
for (const [key, val] of Object.entries(obj)) {
|
|
334
341
|
map.set(key, val)
|
|
@@ -339,25 +346,22 @@ function createStructuredMap(
|
|
|
339
346
|
// Process fields present in the value object
|
|
340
347
|
for (const [key, val] of Object.entries(obj)) {
|
|
341
348
|
if (val === undefined) continue
|
|
342
|
-
const fieldSchema =
|
|
349
|
+
const fieldSchema = productSchema.fields[key]
|
|
343
350
|
const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val
|
|
344
351
|
map.set(key, yjsVal)
|
|
345
352
|
}
|
|
346
353
|
|
|
347
|
-
// Create shared types for
|
|
354
|
+
// Create shared types for first-class CRDT fields declared in the schema
|
|
348
355
|
// but missing from the value object. This ensures Yjs containers
|
|
349
356
|
// exist for later mutation (e.g. .insert() on a text field inside
|
|
350
357
|
// a struct inside a record/list).
|
|
351
358
|
for (const [key, fieldSchema] of Object.entries(
|
|
352
|
-
|
|
359
|
+
productSchema.fields as Record<string, SchemaNode>,
|
|
353
360
|
)) {
|
|
354
361
|
if (key in obj) continue // already processed above
|
|
355
|
-
|
|
356
|
-
if (tag === "text") {
|
|
362
|
+
if (fieldSchema[KIND] === "text") {
|
|
357
363
|
map.set(key, new Y.Text())
|
|
358
364
|
}
|
|
359
|
-
// Other annotated container types (counter, movable, tree) are
|
|
360
|
-
// unsupported in Yjs and will throw if used elsewhere.
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
return map
|
|
@@ -380,7 +384,7 @@ function createStructuredMap(
|
|
|
380
384
|
*
|
|
381
385
|
* @param events - The events from the `observeDeep` callback
|
|
382
386
|
*/
|
|
383
|
-
export function eventsToOps(events: Y.YEvent<any>[]): Op[] {
|
|
387
|
+
export function eventsToOps(events: Y.YEvent<any>[], schema: SchemaNode): Op[] {
|
|
384
388
|
const ops: Op[] = []
|
|
385
389
|
|
|
386
390
|
for (const event of events) {
|
|
@@ -391,7 +395,7 @@ export function eventsToOps(events: Y.YEvent<any>[]): Op[] {
|
|
|
391
395
|
}
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
return expandMapOpsToLeaves(ops)
|
|
398
|
+
return expandMapOpsToLeaves(ops, schema)
|
|
395
399
|
}
|
|
396
400
|
|
|
397
401
|
// ---------------------------------------------------------------------------
|
|
@@ -541,17 +545,6 @@ function extractEventValue(value: unknown): unknown {
|
|
|
541
545
|
// Schema helpers
|
|
542
546
|
// ---------------------------------------------------------------------------
|
|
543
547
|
|
|
544
|
-
/**
|
|
545
|
-
* Unwrap annotation wrappers to reach the structural schema node.
|
|
546
|
-
*/
|
|
547
|
-
function unwrapAnnotations(schema: SchemaNode): SchemaNode {
|
|
548
|
-
let s = schema
|
|
549
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
550
|
-
s = s.schema
|
|
551
|
-
}
|
|
552
|
-
return s
|
|
553
|
-
}
|
|
554
|
-
|
|
555
548
|
/**
|
|
556
549
|
* Resolve the schema at a given path by walking through advanceSchema.
|
|
557
550
|
*/
|
|
@@ -567,8 +560,9 @@ function resolveSchemaAtPath(rootSchema: SchemaNode, path: Path): SchemaNode {
|
|
|
567
560
|
* Get the item schema from a sequence schema, if available.
|
|
568
561
|
*/
|
|
569
562
|
function getItemSchema(schema: SchemaNode): SchemaNode | undefined {
|
|
570
|
-
|
|
571
|
-
|
|
563
|
+
if (schema[KIND] === "sequence") return schema.item
|
|
564
|
+
if (schema[KIND] === "movable") return schema.item
|
|
565
|
+
return undefined
|
|
572
566
|
}
|
|
573
567
|
|
|
574
568
|
/**
|
|
@@ -578,12 +572,14 @@ function getFieldSchema(
|
|
|
578
572
|
schema: SchemaNode,
|
|
579
573
|
key: string,
|
|
580
574
|
): SchemaNode | undefined {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
575
|
+
if (schema[KIND] === "product") {
|
|
576
|
+
return schema.fields[key]
|
|
577
|
+
}
|
|
578
|
+
if (schema[KIND] === "map") {
|
|
579
|
+
return schema.item
|
|
584
580
|
}
|
|
585
|
-
if (
|
|
586
|
-
return
|
|
581
|
+
if (schema[KIND] === "set") {
|
|
582
|
+
return schema.item
|
|
587
583
|
}
|
|
588
584
|
return undefined
|
|
589
585
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,75 +3,60 @@
|
|
|
3
3
|
// Provides a Substrate<YjsVersion> implementation that wraps a Y.Doc
|
|
4
4
|
// with schema-aware typed reads, writes, versioning, and export/import.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Low-level primitives (power users):
|
|
11
|
-
// createYjsSubstrate, yjsSubstrateFactory, yjsReader,
|
|
12
|
-
// resolveYjsType, stepIntoYjs, applyChangeToYjs, eventsToOps, YjsVersion
|
|
6
|
+
// The single entry point is `createDoc(yjs.bind(schema))`. For the
|
|
7
|
+
// batteries-included API, import from this package. For the composable
|
|
8
|
+
// toolkit, import from `@kyneta/schema` directly.
|
|
13
9
|
|
|
14
10
|
// ---------------------------------------------------------------------------
|
|
15
|
-
//
|
|
11
|
+
// Generic API (re-exported from @kyneta/schema for convenience)
|
|
16
12
|
// ---------------------------------------------------------------------------
|
|
17
13
|
|
|
14
|
+
// Types (re-exported for convenience)
|
|
15
|
+
export type { Changeset } from "@kyneta/changefeed"
|
|
16
|
+
export type { DocRef, Op, Ref, SubstratePayload } from "@kyneta/schema"
|
|
17
|
+
// Construction
|
|
18
18
|
// Mutation & observation (re-exported from @kyneta/schema for convenience)
|
|
19
19
|
// Schema definition (re-exported for convenience)
|
|
20
|
+
// Native escape hatch
|
|
21
|
+
// Sync primitives (generic — work for any substrate)
|
|
20
22
|
export {
|
|
21
23
|
applyChanges,
|
|
22
24
|
change,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
subscribeNode,
|
|
26
|
-
} from "@kyneta/schema"
|
|
27
|
-
// Construction
|
|
28
|
-
export { createYjsDoc, createYjsDocFromEntirety } from "./create.js"
|
|
29
|
-
// Sync primitives (Yjs-specific)
|
|
30
|
-
export {
|
|
25
|
+
createDoc,
|
|
26
|
+
createRef,
|
|
31
27
|
exportEntirety,
|
|
32
28
|
exportSince,
|
|
33
29
|
merge,
|
|
30
|
+
NATIVE,
|
|
31
|
+
Schema,
|
|
32
|
+
subscribe,
|
|
33
|
+
subscribeNode,
|
|
34
|
+
unwrap,
|
|
34
35
|
version,
|
|
35
|
-
} from "
|
|
36
|
-
|
|
37
|
-
// Text annotation convenience — so users don't need LoroSchema just for text()
|
|
38
|
-
import type { AnnotatedSchema } from "@kyneta/schema"
|
|
39
|
-
import { Schema } from "@kyneta/schema"
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Collaborative text (CRDT). Produces `annotated("text")`.
|
|
43
|
-
*
|
|
44
|
-
* The annotation implies scalar string semantics for reads,
|
|
45
|
-
* but the Yjs substrate provides collaborative editing (insert, delete)
|
|
46
|
-
* via Y.Text.
|
|
47
|
-
*
|
|
48
|
-
* This is a convenience re-export so that `@kyneta/yjs-schema` users
|
|
49
|
-
* don't need to import `LoroSchema` just for `text()`.
|
|
50
|
-
*/
|
|
51
|
-
export function text(): AnnotatedSchema<"text", undefined> {
|
|
52
|
-
return Schema.annotated("text")
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Types (re-exported for convenience)
|
|
56
|
-
export type { Changeset, Op, Ref, SubstratePayload } from "@kyneta/schema"
|
|
36
|
+
} from "@kyneta/schema"
|
|
57
37
|
|
|
58
38
|
// ---------------------------------------------------------------------------
|
|
59
|
-
//
|
|
39
|
+
// Yjs-specific exports
|
|
60
40
|
// ---------------------------------------------------------------------------
|
|
61
41
|
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
export type { YjsCaps } from "./bind-yjs.js"
|
|
43
|
+
// Namespace
|
|
44
|
+
export { yjs } from "./bind-yjs.js"
|
|
64
45
|
// Change mapping
|
|
65
46
|
export { applyChangeToYjs, eventsToOps } from "./change-mapping.js"
|
|
47
|
+
// NativeMap — the Yjs functor
|
|
48
|
+
export type { YjsNativeMap } from "./native-map.js"
|
|
66
49
|
// Container creation
|
|
67
50
|
export { ensureContainers } from "./populate.js"
|
|
68
51
|
// Reader
|
|
69
52
|
export { yjsReader } from "./reader.js"
|
|
70
53
|
// Substrate
|
|
71
|
-
export {
|
|
54
|
+
export {
|
|
55
|
+
createYjsSubstrate,
|
|
56
|
+
yjsReplicaFactory,
|
|
57
|
+
yjsSubstrateFactory,
|
|
58
|
+
} from "./substrate.js"
|
|
72
59
|
// Version
|
|
73
60
|
export { YjsVersion } from "./version.js"
|
|
74
|
-
// Escape hatch — access the underlying Y.Doc from a ref
|
|
75
|
-
export { yjs } from "./yjs-escape.js"
|
|
76
61
|
// Container resolution
|
|
77
62
|
export { resolveYjsType, stepIntoYjs } from "./yjs-resolve.js"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// native-map — Yjs NativeMap functor.
|
|
2
|
+
//
|
|
3
|
+
// Maps schema kinds to Yjs shared types. Used as the `N`
|
|
4
|
+
// type parameter in `SchemaRef<S, M, N>` for Yjs-backed documents.
|
|
5
|
+
|
|
6
|
+
import type { NativeMap } from "@kyneta/schema"
|
|
7
|
+
import type * as Y from "yjs"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NativeMap for the Yjs CRDT substrate.
|
|
11
|
+
*
|
|
12
|
+
* Maps each schema kind to the corresponding Yjs shared type:
|
|
13
|
+
* - `root → Y.Doc` (the document itself)
|
|
14
|
+
* - `text → Y.Text`
|
|
15
|
+
* - `counter → undefined` (Yjs has no counter type)
|
|
16
|
+
* - `list → Y.Array<unknown>`
|
|
17
|
+
* - `movableList → undefined` (Yjs has no movable list)
|
|
18
|
+
* - `struct → Y.Map<unknown>` (Yjs uses maps for struct fields)
|
|
19
|
+
* - `map → Y.Map<unknown>`
|
|
20
|
+
* - `tree → undefined` (Yjs has no tree type)
|
|
21
|
+
* - `set → undefined` (not yet supported)
|
|
22
|
+
* - `scalar → undefined` (no container; stored in parent map)
|
|
23
|
+
* - `sum → undefined` (no container; stored in parent map)
|
|
24
|
+
*/
|
|
25
|
+
export interface YjsNativeMap extends NativeMap {
|
|
26
|
+
readonly root: Y.Doc
|
|
27
|
+
readonly text: Y.Text
|
|
28
|
+
readonly counter: undefined
|
|
29
|
+
readonly list: Y.Array<unknown>
|
|
30
|
+
readonly movableList: undefined
|
|
31
|
+
readonly struct: Y.Map<unknown>
|
|
32
|
+
readonly map: Y.Map<unknown>
|
|
33
|
+
readonly tree: undefined
|
|
34
|
+
readonly set: undefined
|
|
35
|
+
readonly scalar: undefined
|
|
36
|
+
readonly sum: undefined
|
|
37
|
+
}
|
package/src/populate.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// shared types (Y.Text, Y.Array, Y.Map) and plain value slots uniformly.
|
|
16
16
|
|
|
17
17
|
import type { Schema as SchemaNode } from "@kyneta/schema"
|
|
18
|
-
import { STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema"
|
|
18
|
+
import { KIND, STRUCTURAL_YJS_CLIENT_ID, Zero } from "@kyneta/schema"
|
|
19
19
|
import * as Y from "yjs"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -26,9 +26,9 @@ import * as Y from "yjs"
|
|
|
26
26
|
* Ensure that a Y.Doc's root map contains the correct Yjs shared types
|
|
27
27
|
* matching the schema structure.
|
|
28
28
|
*
|
|
29
|
-
* Obtains the root map via `doc.getMap("root")`,
|
|
30
|
-
* schema, and creates empty containers for each field within a
|
|
31
|
-
* `doc.transact()` call for atomicity.
|
|
29
|
+
* Obtains the root map via `doc.getMap("root")`, reads the root product
|
|
30
|
+
* schema's fields, and creates empty containers for each field within a
|
|
31
|
+
* single `doc.transact()` call for atomicity.
|
|
32
32
|
*
|
|
33
33
|
* When `conditional` is true, fields that already exist in the root map
|
|
34
34
|
* are skipped. This is the correct mode after hydration — containers
|
|
@@ -45,7 +45,7 @@ import * as Y from "yjs"
|
|
|
45
45
|
* structural ops across all peers, enabling Yjs deduplication on merge.
|
|
46
46
|
*
|
|
47
47
|
* @param doc - The Y.Doc to prepare
|
|
48
|
-
* @param schema - The root document schema (
|
|
48
|
+
* @param schema - The root document schema (a ProductSchema)
|
|
49
49
|
* @param conditional - If true, skip fields that already exist in the root map.
|
|
50
50
|
* Context: jj:smmulzkm (two-phase substrate construction)
|
|
51
51
|
*/
|
|
@@ -56,15 +56,7 @@ export function ensureContainers(
|
|
|
56
56
|
): void {
|
|
57
57
|
const rootMap = doc.getMap("root")
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
while (
|
|
61
|
-
rootProduct._kind === "annotated" &&
|
|
62
|
-
rootProduct.schema !== undefined
|
|
63
|
-
) {
|
|
64
|
-
rootProduct = rootProduct.schema
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (rootProduct._kind !== "product") {
|
|
59
|
+
if (schema[KIND] !== "product") {
|
|
68
60
|
return
|
|
69
61
|
}
|
|
70
62
|
|
|
@@ -75,7 +67,7 @@ export function ensureContainers(
|
|
|
75
67
|
|
|
76
68
|
try {
|
|
77
69
|
doc.transact(() => {
|
|
78
|
-
for (const [key, fieldSchema] of Object.entries(
|
|
70
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields).sort(
|
|
79
71
|
([a], [b]) => a.localeCompare(b),
|
|
80
72
|
)) {
|
|
81
73
|
if (conditional && rootMap.has(key)) continue
|
|
@@ -95,62 +87,36 @@ export function ensureContainers(
|
|
|
95
87
|
/**
|
|
96
88
|
* Ensure a root-level Yjs shared type exists for a schema field.
|
|
97
89
|
*
|
|
98
|
-
* Dispatches
|
|
99
|
-
* - `
|
|
100
|
-
* - `
|
|
101
|
-
* - `
|
|
102
|
-
* - `
|
|
103
|
-
* - `
|
|
104
|
-
* - `
|
|
105
|
-
* - `map` → empty Y.Map
|
|
106
|
-
* - `scalar`/`sum` → no-op (plain values don't need containers)
|
|
90
|
+
* Dispatches on `[KIND]`:
|
|
91
|
+
* - `"text"` → empty Y.Text
|
|
92
|
+
* - `"product"` → empty Y.Map (recursive for nested products)
|
|
93
|
+
* - `"sequence"` → empty Y.Array
|
|
94
|
+
* - `"map"` → empty Y.Map
|
|
95
|
+
* - `"scalar"` / `"sum"` → Zero.structural default
|
|
96
|
+
* - `"counter"` / `"set"` / `"tree"` / `"movable"` → throw (not supported by Yjs)
|
|
107
97
|
*/
|
|
108
98
|
function ensureRootField(
|
|
109
99
|
rootMap: Y.Map<unknown>,
|
|
110
100
|
key: string,
|
|
111
101
|
fieldSchema: SchemaNode,
|
|
112
102
|
): void {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
switch (tag) {
|
|
103
|
+
switch (fieldSchema[KIND]) {
|
|
116
104
|
case "text":
|
|
117
105
|
rootMap.set(key, new Y.Text())
|
|
118
106
|
return
|
|
119
107
|
|
|
120
|
-
case "counter":
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Yjs substrate does not support counter annotations. ` +
|
|
123
|
-
`Use Schema.number() with ReplaceChange instead. ` +
|
|
124
|
-
`Encountered counter annotation at root field "${key}".`,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
case "movable":
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Yjs substrate does not support movable list annotations. ` +
|
|
130
|
-
`Yjs has no native movable list type. ` +
|
|
131
|
-
`Encountered movable annotation at root field "${key}".`,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
case "tree":
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Yjs substrate does not support tree annotations. ` +
|
|
137
|
-
`Yjs has no native tree type. ` +
|
|
138
|
-
`Encountered tree annotation at root field "${key}".`,
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const structural = unwrapAnnotations(fieldSchema)
|
|
143
|
-
|
|
144
|
-
switch (structural._kind) {
|
|
145
108
|
case "product":
|
|
146
|
-
rootMap.set(key, ensureMapContainers(
|
|
109
|
+
rootMap.set(key, ensureMapContainers(fieldSchema))
|
|
147
110
|
return
|
|
111
|
+
|
|
148
112
|
case "sequence":
|
|
149
113
|
rootMap.set(key, new Y.Array())
|
|
150
114
|
return
|
|
115
|
+
|
|
151
116
|
case "map":
|
|
152
117
|
rootMap.set(key, new Y.Map())
|
|
153
118
|
return
|
|
119
|
+
|
|
154
120
|
case "scalar":
|
|
155
121
|
case "sum": {
|
|
156
122
|
// Plain values don't need shared type containers, but they DO
|
|
@@ -162,6 +128,16 @@ function ensureRootField(
|
|
|
162
128
|
}
|
|
163
129
|
return
|
|
164
130
|
}
|
|
131
|
+
|
|
132
|
+
case "counter":
|
|
133
|
+
case "set":
|
|
134
|
+
case "tree":
|
|
135
|
+
case "movable":
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". ` +
|
|
138
|
+
`Supported kinds: text, product, sequence, map, scalar, sum. ` +
|
|
139
|
+
`Encountered unsupported kind at root field "${key}".`,
|
|
140
|
+
)
|
|
165
141
|
}
|
|
166
142
|
}
|
|
167
143
|
|
|
@@ -175,37 +151,33 @@ function ensureRootField(
|
|
|
175
151
|
*
|
|
176
152
|
* Only creates containers for fields that require Yjs shared types
|
|
177
153
|
* (text → Y.Text, product → Y.Map, sequence → Y.Array, map → Y.Map).
|
|
178
|
-
* Scalar and sum fields are
|
|
179
|
-
* values via change() when needed.
|
|
154
|
+
* Scalar and sum fields are set to their structural zero defaults.
|
|
180
155
|
*/
|
|
181
156
|
function ensureMapContainers(schema: SchemaNode): Y.Map<unknown> {
|
|
182
157
|
const map = new Y.Map()
|
|
183
|
-
const structural = unwrapAnnotations(schema)
|
|
184
158
|
|
|
185
|
-
if (
|
|
159
|
+
if (schema[KIND] !== "product") return map
|
|
186
160
|
|
|
187
161
|
for (const [key, fieldSchema] of Object.entries(
|
|
188
|
-
|
|
162
|
+
schema.fields as Record<string, SchemaNode>,
|
|
189
163
|
).sort(([a], [b]) => a.localeCompare(b))) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const fs = unwrapAnnotations(fieldSchema)
|
|
164
|
+
switch (fieldSchema[KIND]) {
|
|
165
|
+
case "text":
|
|
166
|
+
map.set(key, new Y.Text())
|
|
167
|
+
break
|
|
198
168
|
|
|
199
|
-
switch (fs._kind) {
|
|
200
169
|
case "product":
|
|
201
170
|
map.set(key, ensureMapContainers(fieldSchema))
|
|
202
171
|
break
|
|
172
|
+
|
|
203
173
|
case "sequence":
|
|
204
174
|
map.set(key, new Y.Array())
|
|
205
175
|
break
|
|
176
|
+
|
|
206
177
|
case "map":
|
|
207
178
|
map.set(key, new Y.Map())
|
|
208
179
|
break
|
|
180
|
+
|
|
209
181
|
case "scalar":
|
|
210
182
|
case "sum": {
|
|
211
183
|
const zero = Zero.structural(fieldSchema)
|
|
@@ -214,23 +186,18 @@ function ensureMapContainers(schema: SchemaNode): Y.Map<unknown> {
|
|
|
214
186
|
}
|
|
215
187
|
break
|
|
216
188
|
}
|
|
189
|
+
|
|
190
|
+
case "counter":
|
|
191
|
+
case "set":
|
|
192
|
+
case "tree":
|
|
193
|
+
case "movable":
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Yjs substrate does not support [KIND]="${fieldSchema[KIND]}". ` +
|
|
196
|
+
`Supported kinds: text, product, sequence, map, scalar, sum. ` +
|
|
197
|
+
`Encountered unsupported kind at nested field "${key}".`,
|
|
198
|
+
)
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
|
|
220
202
|
return map
|
|
221
203
|
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// Helpers
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Unwrap annotation wrappers to reach the structural schema node.
|
|
229
|
-
*/
|
|
230
|
-
function unwrapAnnotations(schema: SchemaNode): SchemaNode {
|
|
231
|
-
let s = schema
|
|
232
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
233
|
-
s = s.schema
|
|
234
|
-
}
|
|
235
|
-
return s
|
|
236
|
-
}
|