@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 +21 -0
- package/README.md +182 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +865 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/bind-yjs.test.ts +266 -0
- package/src/__tests__/create.test.ts +632 -0
- package/src/__tests__/record-text-spike.test.ts +429 -0
- package/src/__tests__/store-reader.test.ts +722 -0
- package/src/__tests__/substrate.test.ts +604 -0
- package/src/__tests__/version.test.ts +227 -0
- package/src/bind-yjs.ts +147 -0
- package/src/change-mapping.ts +612 -0
- package/src/create.ts +172 -0
- package/src/index.ts +83 -0
- package/src/populate.ts +208 -0
- package/src/store-reader.ts +123 -0
- package/src/substrate.ts +252 -0
- package/src/sync.ts +107 -0
- package/src/version.ts +138 -0
- package/src/yjs-escape.ts +100 -0
- package/src/yjs-resolve.ts +108 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|