@kyneta/yjs-schema 1.6.1 → 1.7.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/src/reader.ts DELETED
@@ -1,174 +0,0 @@
1
- // store-reader — YjsReader implementation.
2
- //
3
- // Implements Reader via schema-guided live navigation of the
4
- // Yjs shared type tree. Each read operation resolves the shared type
5
- // at the given path using resolveYjsType, then extracts the
6
- // appropriate value based on `instanceof` discrimination.
7
- //
8
- // Y.Text → .toJSON() (string), Y.Map → .toJSON() (plain object),
9
- // Y.Array → .toJSON() (plain array), plain values → as-is.
10
- //
11
- // Richtext: when the schema at the resolved path is "richtext" and
12
- // the resolved value is a Y.Text, we call .toDelta() and convert to
13
- // RichTextDelta (array of { text, marks? } spans) instead of .toJSON().
14
- //
15
- // Identity-keying: when a SchemaBinding is provided, resolveYjsType
16
- // navigates Y.Map children using identity hashes instead of field names.
17
-
18
- import type {
19
- Path,
20
- Reader,
21
- RichTextDelta,
22
- RichTextSpan,
23
- SchemaBinding,
24
- Schema as SchemaNode,
25
- } from "@kyneta/schema"
26
- import { KIND } from "@kyneta/schema"
27
- import * as Y from "yjs"
28
- import { resolveYjsType } from "./yjs-resolve.js"
29
-
30
- // ---------------------------------------------------------------------------
31
- // Value extraction
32
- // ---------------------------------------------------------------------------
33
-
34
- /**
35
- * Extract a plain value from a Yjs shared type or return a plain value as-is.
36
- *
37
- * - Y.Text → `.toJSON()` (string)
38
- * - Y.Map → `.toJSON()` (plain object snapshot — for product/map reads)
39
- * - Y.Array → `.toJSON()` (plain array snapshot)
40
- * - Plain values (string, number, boolean, null) → returned as-is
41
- */
42
- function extractValue(resolved: unknown): unknown {
43
- if (resolved instanceof Y.Text) {
44
- return resolved.toJSON()
45
- }
46
- if (resolved instanceof Y.Map) {
47
- return resolved.toJSON()
48
- }
49
- if (resolved instanceof Y.Array) {
50
- return resolved.toJSON()
51
- }
52
- // Plain scalar value (string, number, boolean, null, etc.)
53
- return resolved
54
- }
55
-
56
- // ---------------------------------------------------------------------------
57
- // Rich text delta conversion
58
- // ---------------------------------------------------------------------------
59
-
60
- /**
61
- * Convert a Y.Text's delta (Quill format) to a kyneta RichTextDelta.
62
- *
63
- * Yjs `.toDelta()` returns `{ insert: string, attributes?: Record<string, any> }[]`.
64
- * Kyneta RichTextDelta is `{ text: string, marks?: MarkMap }[]`.
65
- */
66
- function yTextToRichTextDelta(ytext: Y.Text): RichTextDelta {
67
- const delta = ytext.toDelta() as Array<{
68
- insert: string
69
- attributes?: Record<string, unknown>
70
- }>
71
- const spans: RichTextSpan[] = []
72
- for (const d of delta) {
73
- if (typeof d.insert !== "string") continue
74
- const span: RichTextSpan =
75
- d.attributes && Object.keys(d.attributes).length > 0
76
- ? { text: d.insert, marks: d.attributes }
77
- : { text: d.insert }
78
- spans.push(span)
79
- }
80
- return spans
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // yjsReader
85
- // ---------------------------------------------------------------------------
86
-
87
- /**
88
- * Creates a Reader that navigates the Yjs shared type tree live,
89
- * using the schema as a type witness to determine navigation at each
90
- * path segment.
91
- *
92
- * The reader is a live view — mutations to the underlying Y.Doc
93
- * (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
94
- * visible through the reader.
95
- *
96
- * Internally obtains the root map via `doc.getMap("root")`.
97
- *
98
- * @param doc - The Y.Doc to read from.
99
- * @param schema - The root schema for the document.
100
- * @param binding - Optional SchemaBinding for identity-keyed navigation.
101
- */
102
- export function yjsReader(
103
- doc: Y.Doc,
104
- schema: SchemaNode,
105
- binding?: SchemaBinding,
106
- ): Reader {
107
- const rootMap = doc.getMap("root")
108
-
109
- return {
110
- read(path: Path): unknown {
111
- if (path.length === 0) {
112
- // Root read — return the full root map as JSON
113
- return rootMap.toJSON()
114
- }
115
- const { resolved, schema: nodeSchema } = resolveYjsType(
116
- rootMap,
117
- schema,
118
- path,
119
- binding,
120
- )
121
- // Richtext: Y.Text at a richtext schema position → RichTextDelta
122
- if (nodeSchema[KIND] === "richtext" && resolved instanceof Y.Text) {
123
- return yTextToRichTextDelta(resolved)
124
- }
125
- return extractValue(resolved)
126
- },
127
-
128
- arrayLength(path: Path): number {
129
- const { resolved } = resolveYjsType(rootMap, schema, path, binding)
130
- if (resolved instanceof Y.Array) {
131
- return resolved.length
132
- }
133
- // Graceful fallback for plain array values
134
- if (Array.isArray(resolved)) {
135
- return resolved.length
136
- }
137
- return 0
138
- },
139
-
140
- keys(path: Path): string[] {
141
- const { resolved } = resolveYjsType(rootMap, schema, path, binding)
142
- if (resolved instanceof Y.Map) {
143
- return Array.from(resolved.keys())
144
- }
145
- // Graceful fallback for plain object values
146
- if (
147
- resolved !== null &&
148
- resolved !== undefined &&
149
- typeof resolved === "object" &&
150
- !Array.isArray(resolved)
151
- ) {
152
- return Object.keys(resolved as Record<string, unknown>)
153
- }
154
- return []
155
- },
156
-
157
- hasKey(path: Path, key: string): boolean {
158
- const { resolved } = resolveYjsType(rootMap, schema, path, binding)
159
- if (resolved instanceof Y.Map) {
160
- return resolved.has(key)
161
- }
162
- // Graceful fallback for plain object values
163
- if (
164
- resolved !== null &&
165
- resolved !== undefined &&
166
- typeof resolved === "object" &&
167
- !Array.isArray(resolved)
168
- ) {
169
- return key in (resolved as Record<string, unknown>)
170
- }
171
- return false
172
- },
173
- }
174
- }