@kyneta/yjs-schema 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Duane Johnson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # @kyneta/yjs-schema
2
+
3
+ Yjs CRDT substrate for `@kyneta/schema` — collaborative data types with typed refs.
4
+
5
+ Wraps a `Y.Doc` with schema-aware typed reads, writes, versioning, and export/import through the standard `Substrate<YjsVersion>` interface. Adding a Yjs substrate proves the schema algebra's portability beyond Loro and opens the door to the entire Yjs ecosystem (y-websocket, y-indexeddb, y-webrtc, Hocuspocus, Liveblocks, etc.).
6
+
7
+ ## Quick Start
8
+
9
+ ```ts
10
+ import {
11
+ createYjsDoc,
12
+ change,
13
+ subscribe,
14
+ Schema,
15
+ text,
16
+ version,
17
+ exportSnapshot,
18
+ exportSince,
19
+ importDelta,
20
+ } from "@kyneta/yjs-schema"
21
+
22
+ // Define a schema
23
+ const TodoDoc = Schema.doc({
24
+ title: text(),
25
+ items: Schema.list(
26
+ Schema.struct({
27
+ name: Schema.string(),
28
+ done: Schema.boolean(),
29
+ }),
30
+ ),
31
+ })
32
+
33
+ // Create a document with optional seed values
34
+ const doc = createYjsDoc(TodoDoc, {
35
+ title: "My Todos",
36
+ items: [{ name: "Buy milk", done: false }],
37
+ })
38
+
39
+ // Read
40
+ doc.title() // "My Todos"
41
+ doc.items.length // 1
42
+
43
+ // Write
44
+ change(doc, (d) => {
45
+ d.title.insert(9, " (v2)")
46
+ d.items.push({ name: "Walk dog", done: false })
47
+ })
48
+
49
+ // Observe
50
+ subscribe(doc, (changeset) => {
51
+ console.log("Changed:", changeset.ops.length, "ops")
52
+ })
53
+ ```
54
+
55
+ ## Schema Types Supported
56
+
57
+ | Schema type | Yjs backing type | Notes |
58
+ |---|---|---|
59
+ | `text()` | `Y.Text` | Character-level collaborative editing |
60
+ | `Schema.struct({...})` | `Y.Map` | Fixed-key product type |
61
+ | `Schema.list(item)` | `Y.Array` | Ordered sequence |
62
+ | `Schema.record(item)` | `Y.Map` | Dynamic-key map |
63
+ | `Schema.string()` | Plain value | Stored in parent `Y.Map` |
64
+ | `Schema.number()` | Plain value | Stored in parent `Y.Map` |
65
+ | `Schema.boolean()` | Plain value | Stored in parent `Y.Map` |
66
+
67
+ ### Unsupported
68
+
69
+ - **`Schema.annotated("counter")`** — Yjs has no native counter type. Use `Schema.number()` with `ReplaceChange` instead. Attempting to use a counter annotation will throw at construction time.
70
+ - **`Schema.annotated("movable")`** — Yjs has no native movable list. Will throw at construction time.
71
+ - **`Schema.annotated("tree")`** — Yjs has no native tree type. Will throw at construction time.
72
+
73
+ ## Sync
74
+
75
+ ```ts
76
+ import {
77
+ createYjsDoc,
78
+ createYjsDocFromSnapshot,
79
+ version,
80
+ exportSnapshot,
81
+ exportSince,
82
+ importDelta,
83
+ change,
84
+ } from "@kyneta/yjs-schema"
85
+
86
+ // Peer A creates a doc
87
+ const docA = createYjsDoc(MySchema, { title: "Draft" })
88
+
89
+ // Peer B bootstraps from a full snapshot
90
+ const snapshot = exportSnapshot(docA)
91
+ const docB = createYjsDocFromSnapshot(MySchema, snapshot)
92
+
93
+ // After mutations on A, sync incrementally
94
+ const vBefore = version(docB)
95
+ change(docA, (d) => d.title.insert(5, " v2"))
96
+
97
+ const delta = exportSince(docA, vBefore)
98
+ importDelta(docB, delta!)
99
+ // docB.title() === "Draft v2"
100
+ ```
101
+
102
+ ## Exchange Integration
103
+
104
+ ```ts
105
+ import { bindYjs } from "@kyneta/yjs-schema"
106
+ import { Schema, text } from "@kyneta/yjs-schema"
107
+
108
+ const TodoDoc = bindYjs(Schema.doc({
109
+ title: text(),
110
+ items: Schema.list(Schema.struct({
111
+ name: Schema.string(),
112
+ done: Schema.boolean(),
113
+ })),
114
+ }))
115
+
116
+ // Use with @kyneta/exchange
117
+ const doc = exchange.get("my-todos", TodoDoc)
118
+ ```
119
+
120
+ `bindYjs` produces a `BoundSchema` with `strategy: "causal"`, which the exchange uses for bidirectional CRDT sync.
121
+
122
+ ## Escape Hatch
123
+
124
+ Access the underlying `Y.Doc` for direct Yjs API usage:
125
+
126
+ ```ts
127
+ import { yjs } from "@kyneta/yjs-schema"
128
+
129
+ const doc = createYjsDoc(MySchema)
130
+ const yjsDoc = yjs(doc)
131
+
132
+ // Use with Yjs ecosystem
133
+ // y-websocket, y-indexeddb, y-webrtc, Hocuspocus, etc.
134
+ yjsDoc.getMap("root").toJSON() // raw state
135
+ yjsDoc.clientID // client ID
136
+ ```
137
+
138
+ ## Yjs Ecosystem Compatibility
139
+
140
+ Because `yjs(doc)` returns a standard `Y.Doc`, the entire Yjs provider ecosystem works out of the box:
141
+
142
+ - **y-websocket** — WebSocket sync
143
+ - **y-indexeddb** — Local persistence
144
+ - **y-webrtc** — Peer-to-peer sync
145
+ - **Hocuspocus** — Scalable Yjs server
146
+ - **Liveblocks** — Managed collaboration infrastructure
147
+ - **y-prosemirror** / **y-codemirror** — Rich text editor bindings
148
+
149
+ ## API Reference
150
+
151
+ ### Batteries-included (most users)
152
+
153
+ | Export | Description |
154
+ |---|---|
155
+ | `createYjsDoc(schema, docOrSeed?)` | Create a live Yjs-backed document |
156
+ | `createYjsDocFromSnapshot(schema, payload)` | Reconstruct from snapshot |
157
+ | `version(doc)` | Current `YjsVersion` |
158
+ | `exportSnapshot(doc)` | Full state as `SubstratePayload` |
159
+ | `exportSince(doc, since)` | Delta since version |
160
+ | `importDelta(doc, payload, origin?)` | Apply delta from peer |
161
+ | `change(doc, fn)` | Transactional mutation |
162
+ | `subscribe(doc, callback)` | Observe changes |
163
+ | `bindYjs(schema)` | Bind schema for exchange use |
164
+ | `yjs(ref)` | Escape hatch → `Y.Doc` |
165
+ | `text()` | `Schema.annotated("text")` convenience |
166
+
167
+ ### Low-level primitives (power users)
168
+
169
+ | Export | Description |
170
+ |---|---|
171
+ | `YjsVersion` | Version class wrapping Yjs state vectors |
172
+ | `yjsStoreReader(doc, schema)` | Live `StoreReader` over Yjs types |
173
+ | `resolveYjsType(rootMap, schema, path)` | Path resolution |
174
+ | `applyChangeToYjs(rootMap, schema, path, change)` | kyneta → Yjs |
175
+ | `eventsToOps(events)` | Yjs → kyneta |
176
+ | `populateRoot(doc, schema, seed)` | Root container population |
177
+ | `createYjsSubstrate(doc, schema)` | Low-level substrate construction |
178
+ | `yjsSubstrateFactory` | `SubstrateFactory<YjsVersion>` |
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,351 @@
1
+ import { Version, Schema, Ref, SubstratePayload, StoreReader, Path, Segment, ChangeBase, Op, Substrate, SubstrateFactory, BoundSchema, AnnotatedSchema } from '@kyneta/schema';
2
+ export { Changeset, Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
3
+ import * as Y from 'yjs';
4
+ import { Doc } from 'yjs';
5
+
6
+ /**
7
+ * A Version wrapping a Yjs state vector.
8
+ *
9
+ * State vectors track the complete peer state — which operations from
10
+ * each client have been observed. This is the right abstraction for sync
11
+ * diffing: `exportSince(version)` uses the state vector to compute the
12
+ * minimal update payload via `Y.encodeStateAsUpdate(doc, sv)`.
13
+ *
14
+ * `serialize()` encodes to base64 for text-safe embedding.
15
+ * `compare()` decodes both state vectors and performs standard
16
+ * version-vector partial-order comparison over the client-clock maps.
17
+ */
18
+ declare class YjsVersion implements Version {
19
+ readonly sv: Uint8Array;
20
+ constructor(sv: Uint8Array);
21
+ /**
22
+ * Serialize the state vector to a base64 string.
23
+ *
24
+ * The encoding is: raw state vector bytes → base64.
25
+ * This is text-safe for embedding in HTML meta tags, URL parameters, etc.
26
+ */
27
+ serialize(): string;
28
+ /**
29
+ * Compare with another version using version-vector partial order.
30
+ *
31
+ * Decodes both state vectors via `Y.decodeStateVector()` to get
32
+ * `Map<number, number>` (clientID → clock), then compares:
33
+ *
34
+ * - Collect the union of all client IDs from both maps.
35
+ * - For each client, compare clocks (missing client = clock 0).
36
+ * - If all clocks in `this` ≤ `other` and at least one strictly less → `"behind"`
37
+ * - If all clocks in `this` ≥ `other` and at least one strictly greater → `"ahead"`
38
+ * - If all clocks equal → `"equal"`
39
+ * - Otherwise → `"concurrent"`
40
+ *
41
+ * Throws if `other` is not a `YjsVersion`.
42
+ */
43
+ compare(other: Version): "behind" | "equal" | "ahead" | "concurrent";
44
+ /**
45
+ * Parse a serialized YjsVersion string back into a YjsVersion.
46
+ *
47
+ * The inverse of `serialize()`: base64 → `Uint8Array`.
48
+ */
49
+ static parse(serialized: string): YjsVersion;
50
+ }
51
+
52
+ /**
53
+ * Create a live Yjs-backed document.
54
+ *
55
+ * **Form 1 — bring your own doc:**
56
+ * ```ts
57
+ * const yjsDoc = new Y.Doc()
58
+ * const doc = createYjsDoc(mySchema, yjsDoc)
59
+ * ```
60
+ *
61
+ * **Form 2 — fresh empty doc:**
62
+ * ```ts
63
+ * const doc = createYjsDoc(mySchema)
64
+ *
65
+ * // Apply initial content via change():
66
+ * change(doc, d => {
67
+ * d.title.insert(0, "Hello")
68
+ * d.items.push({ name: "First item" })
69
+ * })
70
+ * ```
71
+ *
72
+ * Returns a full-stack `Ref<S>` — callable, navigable, writable,
73
+ * transactable, and observable. Backed by a `YjsSubstrate` with
74
+ * CRDT collaboration support.
75
+ *
76
+ * The returned ref observes **all** mutations to the underlying Y.Doc,
77
+ * regardless of source (local kyneta writes, importDelta, external
78
+ * `Y.applyUpdate()`, external raw Yjs API mutations).
79
+ *
80
+ * @param schema - The schema describing the document structure.
81
+ * @param doc - Optional `Y.Doc` instance to wrap. If omitted, a fresh
82
+ * empty Y.Doc is created with containers matching the schema.
83
+ */
84
+ type CreateYjsDoc = <S extends Schema>(schema: S, doc?: Y.Doc) => Ref<S>;
85
+ declare const createYjsDoc: CreateYjsDoc;
86
+ type CreateYjsDocFromSnapshot = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
87
+ /**
88
+ * Reconstruct a live Yjs-backed document from a substrate snapshot payload.
89
+ *
90
+ * The payload must have been produced by `exportSnapshot()` on a
91
+ * compatible document. This is the entry point for SSR hydration
92
+ * and reconnection past log compaction.
93
+ *
94
+ * ```ts
95
+ * const payload = exportSnapshot(docA)
96
+ * const docB = createYjsDocFromSnapshot(MySchema, payload)
97
+ * // docB has the same state as docA at the time of export
98
+ * ```
99
+ */
100
+ declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
101
+
102
+ /**
103
+ * Current version as a `YjsVersion` (wrapping a Yjs state vector).
104
+ *
105
+ * Use `.serialize()` to get a text-safe string for embedding in HTML
106
+ * meta tags, URL parameters, etc.
107
+ *
108
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
109
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
110
+ */
111
+ declare function version(doc: object): YjsVersion;
112
+ /**
113
+ * Export the full substrate snapshot — sufficient for a new peer to
114
+ * reconstruct an equivalent document via `createYjsDocFromSnapshot()`.
115
+ *
116
+ * Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
117
+ *
118
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
119
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
120
+ */
121
+ declare function exportSnapshot(doc: object): SubstratePayload;
122
+ /**
123
+ * Export a delta payload containing all changes since the given version.
124
+ *
125
+ * Returns a binary `SubstratePayload` (Yjs update bytes), or `null`
126
+ * if the delta cannot be computed.
127
+ *
128
+ * ```ts
129
+ * const v0 = version(docA)
130
+ * change(docA, d => d.title.insert(0, "Hi"))
131
+ * const delta = exportSince(docA, v0)
132
+ * importDelta(docB, delta!)
133
+ * ```
134
+ *
135
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
136
+ * @param since - The version to diff from.
137
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
138
+ */
139
+ declare function exportSince(doc: object, since: YjsVersion): SubstratePayload | null;
140
+ /**
141
+ * Import a delta payload into a live document.
142
+ *
143
+ * The payload must have been produced by `exportSince()` or
144
+ * `exportSnapshot()` on a compatible document.
145
+ *
146
+ * After import, the changefeed fires for all subscribers — the event
147
+ * bridge handles this automatically.
148
+ *
149
+ * ```ts
150
+ * const delta = exportSince(docA, sinceVersion)
151
+ * importDelta(docB, delta!, "sync")
152
+ * ```
153
+ *
154
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
155
+ * @param payload - The delta or snapshot payload to import.
156
+ * @param origin - Optional provenance tag for the changeset.
157
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
158
+ */
159
+ declare function importDelta(doc: object, payload: SubstratePayload, origin?: string): void;
160
+
161
+ /**
162
+ * Creates a StoreReader that navigates the Yjs shared type tree live,
163
+ * using the schema as a type witness to determine navigation at each
164
+ * path segment.
165
+ *
166
+ * The reader is a live view — mutations to the underlying Y.Doc
167
+ * (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
168
+ * visible through the reader.
169
+ *
170
+ * Internally obtains the root map via `doc.getMap("root")`.
171
+ *
172
+ * @param doc - The Y.Doc to read from.
173
+ * @param schema - The root schema for the document.
174
+ */
175
+ declare function yjsStoreReader(doc: Y.Doc, schema: Schema): StoreReader;
176
+
177
+ /**
178
+ * Navigate one step deeper into the Yjs shared type tree.
179
+ *
180
+ * Uses `instanceof` for runtime type discrimination:
181
+ * - `Y.Map` → `.get(key)`
182
+ * - `Y.Array` → `.get(index)`
183
+ * - `Y.Text` → terminal (cannot step further)
184
+ * - Plain value → terminal (return `undefined`)
185
+ *
186
+ * @param current - The current position (a Yjs shared type or plain value)
187
+ * @param segment - The path segment to follow
188
+ */
189
+ declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
190
+ /**
191
+ * Resolve a Yjs shared type (or plain value) at the given path.
192
+ *
193
+ * Left-folds over path segments using `advanceSchema` for pure schema
194
+ * descent and `stepIntoYjs` for Yjs-specific navigation.
195
+ *
196
+ * Returns the Yjs shared type or plain value at the terminal position.
197
+ * For an empty path, returns the root map itself.
198
+ *
199
+ * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
200
+ * @param rootSchema - The root document schema
201
+ * @param path - The path to resolve
202
+ */
203
+ declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
204
+
205
+ /**
206
+ * Apply a kyneta Change to the Yjs shared type tree imperatively.
207
+ *
208
+ * Resolves the target shared type at `path`, then applies the change
209
+ * via the appropriate Yjs API. Must be called within a `doc.transact()`
210
+ * for atomicity and correct event batching.
211
+ *
212
+ * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
213
+ * @param rootSchema - The root document schema
214
+ * @param path - The path to the target
215
+ * @param change - The kyneta Change to apply
216
+ */
217
+ declare function applyChangeToYjs(rootMap: Y.Map<any>, rootSchema: Schema, path: Path, change: ChangeBase): void;
218
+ /**
219
+ * Convert `observeDeep` events into kyneta `Op[]` for changefeed delivery.
220
+ *
221
+ * Each `Y.YEvent` in the array maps to one Op with:
222
+ * - `path`: derived from `event.path` (relative to the observed root Y.Map)
223
+ * - `change`: derived from the event's delta/keys based on target type
224
+ *
225
+ * `event.path` in `observeDeep` is relative to the observed shared type.
226
+ * Since we observe `rootMap` (the single root Y.Map), paths map directly
227
+ * to kyneta `PathSegment[]`.
228
+ *
229
+ * @param events - The events from the `observeDeep` callback
230
+ */
231
+ declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
232
+
233
+ /**
234
+ * Ensure that a Y.Doc's root map contains the correct Yjs shared types
235
+ * matching the schema structure.
236
+ *
237
+ * Obtains the root map via `doc.getMap("root")`, unwraps the root product
238
+ * schema, and creates empty containers for each field within a single
239
+ * `doc.transact()` call for atomicity.
240
+ *
241
+ * No values are written — the containers are empty after this call.
242
+ * Initial content should be applied via `change()` after substrate
243
+ * construction.
244
+ *
245
+ * @param doc - The Y.Doc to prepare
246
+ * @param schema - The root document schema (typically annotated("doc", product))
247
+ */
248
+ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
249
+
250
+ /**
251
+ * Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
252
+ *
253
+ * This is the "bring your own doc" entry point. The user creates and
254
+ * manages the Y.Doc (possibly via a Yjs provider); this function wraps
255
+ * it with a schema-aware overlay providing typed reads, writes,
256
+ * versioning, and export/import through the standard Substrate interface.
257
+ *
258
+ * **Event bridge contract:** A persistent `observeDeep` handler is
259
+ * registered on the root Y.Map at construction time. All non-kyneta
260
+ * mutations to the Y.Doc (imports, external local writes) are bridged
261
+ * to the kyneta changefeed. Subscribing to the kyneta doc observes all
262
+ * mutations regardless of source.
263
+ *
264
+ * @param doc - The Y.Doc to wrap. The substrate does NOT own the doc;
265
+ * the caller is responsible for its lifecycle.
266
+ * @param schema - The root schema for the document.
267
+ */
268
+ declare function createYjsSubstrate(doc: Y.Doc, schema: Schema): Substrate<YjsVersion>;
269
+ /**
270
+ * Factory for constructing Yjs-backed substrates.
271
+ *
272
+ * - `create(schema)` — creates a fresh Y.Doc with empty containers
273
+ * matching the schema structure. No seed data — initial content
274
+ * should be applied via `change()` after construction.
275
+ * - `fromSnapshot(payload, schema)` — creates a Y.Doc from a snapshot
276
+ * payload, returns a substrate.
277
+ * - `parseVersion(serialized)` — deserializes a YjsVersion.
278
+ */
279
+ declare const yjsSubstrateFactory: SubstrateFactory<YjsVersion>;
280
+
281
+ /**
282
+ * Bind a schema to the Yjs CRDT substrate with causal merge strategy.
283
+ *
284
+ * This is the recommended way to declare a Yjs-backed document type.
285
+ * The factory builder injects a deterministic numeric Yjs clientID derived
286
+ * from the exchange's string peerId, ensuring consistent change attribution
287
+ * across all documents and sessions.
288
+ *
289
+ * **Unsupported annotations:** Yjs has no native counter, movable list,
290
+ * or tree types. Schemas passed to `bindYjs` must not contain
291
+ * `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
292
+ * `Schema.annotated("tree")`. These will throw at construction time.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * import { bindYjs } from "@kyneta/yjs-schema"
297
+ * import { Schema } from "@kyneta/schema"
298
+ *
299
+ * const TodoDoc = bindYjs(Schema.doc({
300
+ * title: Schema.annotated("text"),
301
+ * items: Schema.list(Schema.struct({
302
+ * name: Schema.string(),
303
+ * done: Schema.boolean(),
304
+ * })),
305
+ * }))
306
+ *
307
+ * const doc = exchange.get("my-todos", TodoDoc)
308
+ * ```
309
+ */
310
+ declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
311
+
312
+ /**
313
+ * Returns the `Y.Doc` backing the given ref.
314
+ *
315
+ * This is the Yjs-specific escape hatch for accessing substrate-level
316
+ * capabilities: raw Yjs API, y-prosemirror/y-codemirror bindings,
317
+ * undo manager, awareness protocol, Yjs providers (y-websocket,
318
+ * y-indexeddb, y-webrtc, Hocuspocus, Liveblocks), etc.
319
+ *
320
+ * Currently supports root document refs only. Child-level resolution
321
+ * (e.g. `yjs(doc.title)` → `Y.Text`) is future work.
322
+ *
323
+ * @param ref - A root document ref backed by a Yjs substrate
324
+ * @returns The `Y.Doc` backing the ref
325
+ * @throws If the ref is not backed by a Yjs substrate
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * import { yjs } from "@kyneta/yjs-schema"
330
+ *
331
+ * const doc = exchange.get("my-doc", TodoDoc)
332
+ * const yjsDoc = yjs(doc)
333
+ * console.log(yjsDoc.getMap("root").toJSON()) // raw state
334
+ * console.log(yjsDoc.clientID) // client ID
335
+ * ```
336
+ */
337
+ declare function yjs(ref: object): Doc;
338
+
339
+ /**
340
+ * Collaborative text (CRDT). Produces `annotated("text")`.
341
+ *
342
+ * The annotation implies scalar string semantics for reads,
343
+ * but the Yjs substrate provides collaborative editing (insert, delete)
344
+ * via Y.Text.
345
+ *
346
+ * This is a convenience re-export so that `@kyneta/yjs-schema` users
347
+ * don't need to import `LoroSchema` just for `text()`.
348
+ */
349
+ declare function text(): AnnotatedSchema<"text", undefined>;
350
+
351
+ export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromSnapshot, createYjsSubstrate, ensureContainers, eventsToOps, exportSince, exportSnapshot, importDelta, resolveYjsType, stepIntoYjs, text, version, yjs, yjsStoreReader, yjsSubstrateFactory };