@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 CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "@kyneta/yjs-schema"
21
21
 
22
22
  // Define a schema
23
- const TodoDoc = Schema.doc({
23
+ const TodoDoc = Schema.struct({
24
24
  title: text(),
25
25
  items: Schema.list(
26
26
  Schema.struct({
@@ -105,7 +105,7 @@ importDelta(docB, delta!)
105
105
  import { bindYjs } from "@kyneta/yjs-schema"
106
106
  import { Schema, text } from "@kyneta/yjs-schema"
107
107
 
108
- const TodoDoc = bindYjs(Schema.doc({
108
+ const TodoDoc = bindYjs(Schema.struct({
109
109
  title: text(),
110
110
  items: Schema.list(Schema.struct({
111
111
  name: Schema.string(),
@@ -179,4 +179,4 @@ Because `yjs(doc)` returns a standard `Y.Doc`, the entire Yjs provider ecosystem
179
179
 
180
180
  ## License
181
181
 
182
- MIT
182
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,193 +1,54 @@
1
- import { Version, Schema, Ref, SubstratePayload, BoundSchema, Path, ChangeBase, Op, Reader, Substrate, SubstrateFactory, Segment, AnnotatedSchema } from '@kyneta/schema';
2
- export { Changeset, Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
1
+ export { Changeset } from '@kyneta/changefeed';
2
+ import { NativeMap, SubstrateNamespace, CrdtStrategy, Schema, Path, ChangeBase, Op, Reader, Version, Substrate, ReplicaFactory, SubstrateFactory, Segment } from '@kyneta/schema';
3
+ export { DocRef, NATIVE, Op, Ref, Schema, SubstratePayload, applyChanges, change, createDoc, createRef, exportEntirety, exportSince, merge, subscribe, subscribeNode, unwrap, version } from '@kyneta/schema';
3
4
  import * as Y from 'yjs';
4
- import { Doc } from 'yjs';
5
5
 
6
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;
7
+ * NativeMap for the Yjs CRDT substrate.
8
+ *
9
+ * Maps each schema kind to the corresponding Yjs shared type:
10
+ * - `root Y.Doc` (the document itself)
11
+ * - `text Y.Text`
12
+ * - `counter undefined` (Yjs has no counter type)
13
+ * - `list → Y.Array<unknown>`
14
+ * - `movableList → undefined` (Yjs has no movable list)
15
+ * - `struct Y.Map<unknown>` (Yjs uses maps for struct fields)
16
+ * - `map Y.Map<unknown>`
17
+ * - `tree → undefined` (Yjs has no tree type)
18
+ * - `set undefined` (not yet supported)
19
+ * - `scalar → undefined` (no container; stored in parent map)
20
+ * - `sum → undefined` (no container; stored in parent map)
21
+ */
22
+ interface YjsNativeMap extends NativeMap {
23
+ readonly root: Y.Doc;
24
+ readonly text: Y.Text;
25
+ readonly counter: undefined;
26
+ readonly list: Y.Array<unknown>;
27
+ readonly movableList: undefined;
28
+ readonly struct: Y.Map<unknown>;
29
+ readonly map: Y.Map<unknown>;
30
+ readonly tree: undefined;
31
+ readonly set: undefined;
32
+ readonly scalar: undefined;
33
+ readonly sum: undefined;
50
34
  }
51
35
 
52
36
  /**
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, merge, 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 CreateYjsDocFromEntirety = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
87
- /**
88
- * Reconstruct a live Yjs-backed document from a substrate entirety payload.
89
- *
90
- * The payload must have been produced by `exportEntirety()` 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 = exportEntirety(docA)
96
- * const docB = createYjsDocFromEntirety(MySchema, payload)
97
- * // docB has the same state as docA at the time of export
98
- * ```
99
- */
100
- declare const createYjsDocFromEntirety: CreateYjsDocFromEntirety;
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 `createYjsDocFromEntirety`.
109
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
110
- */
111
- declare function version(doc: object): YjsVersion;
112
- /**
113
- * Export the full substrate entirety — sufficient for a new peer to
114
- * reconstruct an equivalent document via `createYjsDocFromEntirety()`.
115
- *
116
- * Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
117
- *
118
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
119
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
120
- */
121
- declare function exportEntirety(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
- * merge(docB, delta!)
133
- * ```
134
- *
135
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
136
- * @param since - The version to diff from.
137
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
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
- * `exportEntirety()` 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
- * merge(docB, delta!, "sync")
152
- * ```
153
- *
154
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
155
- * @param payload - The delta or entirety payload to merge.
156
- * @param origin - Optional provenance tag for the changeset.
157
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
158
- */
159
- declare function merge(doc: object, payload: SubstratePayload, origin?: string): void;
160
-
161
- /**
162
- * Bind a schema to the Yjs CRDT substrate with causal merge strategy.
163
- *
164
- * This is the recommended way to declare a Yjs-backed document type.
165
- * The factory builder injects a deterministic numeric Yjs clientID derived
166
- * from the exchange's string peerId, ensuring consistent change attribution
167
- * across all documents and sessions.
168
- *
169
- * **Unsupported annotations:** Yjs has no native counter, movable list,
170
- * or tree types. Schemas passed to `bindYjs` must not contain
171
- * `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
172
- * `Schema.annotated("tree")`. These will throw at construction time.
37
+ * The Yjs CRDT substrate namespace.
173
38
  *
174
- * @example
175
- * ```ts
176
- * import { bindYjs } from "@kyneta/yjs-schema"
177
- * import { Schema } from "@kyneta/schema"
39
+ * - `yjs.bind(schema)` — collaborative sync (default)
40
+ * - `yjs.bind(schema, "ephemeral")` — ephemeral/presence broadcast
41
+ * - `yjs.replica()` collaborative replication (default)
42
+ * - `yjs.replica("ephemeral")` ephemeral replication
178
43
  *
179
- * const TodoDoc = bindYjs(Schema.doc({
180
- * title: Schema.annotated("text"),
181
- * items: Schema.list(Schema.struct({
182
- * name: Schema.string(),
183
- * done: Schema.boolean(),
184
- * })),
185
- * }))
44
+ * Strategy is constrained to `CrdtStrategy` (`"collaborative" | "ephemeral"`).
45
+ * Passing `"authoritative"` is a compile error.
186
46
  *
187
- * const doc = exchange.get("my-todos", TodoDoc)
188
- * ```
47
+ * To access the underlying Y.Doc, use `unwrap(ref)` from `@kyneta/schema`.
189
48
  */
190
- declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
49
+ /** The closed set of capability tags that the Yjs substrate supports. */
50
+ type YjsCaps = "text" | "json";
51
+ declare const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps, YjsNativeMap>;
191
52
 
192
53
  /**
193
54
  * Apply a kyneta Change to the Yjs shared type tree imperatively.
@@ -215,15 +76,15 @@ declare function applyChangeToYjs(rootMap: Y.Map<any>, rootSchema: Schema, path:
215
76
  *
216
77
  * @param events - The events from the `observeDeep` callback
217
78
  */
218
- declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
79
+ declare function eventsToOps(events: Y.YEvent<any>[], schema: Schema): Op[];
219
80
 
220
81
  /**
221
82
  * Ensure that a Y.Doc's root map contains the correct Yjs shared types
222
83
  * matching the schema structure.
223
84
  *
224
- * Obtains the root map via `doc.getMap("root")`, unwraps the root product
225
- * schema, and creates empty containers for each field within a single
226
- * `doc.transact()` call for atomicity.
85
+ * Obtains the root map via `doc.getMap("root")`, reads the root product
86
+ * schema's fields, and creates empty containers for each field within a
87
+ * single `doc.transact()` call for atomicity.
227
88
  *
228
89
  * When `conditional` is true, fields that already exist in the root map
229
90
  * are skipped. This is the correct mode after hydration — containers
@@ -240,7 +101,7 @@ declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
240
101
  * structural ops across all peers, enabling Yjs deduplication on merge.
241
102
  *
242
103
  * @param doc - The Y.Doc to prepare
243
- * @param schema - The root document schema (typically annotated("doc", product))
104
+ * @param schema - The root document schema (a ProductSchema)
244
105
  * @param conditional - If true, skip fields that already exist in the root map.
245
106
  * Context: jj:smmulzkm (two-phase substrate construction)
246
107
  */
@@ -262,6 +123,55 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema, conditional?: bool
262
123
  */
263
124
  declare function yjsReader(doc: Y.Doc, schema: Schema): Reader;
264
125
 
126
+ /**
127
+ * A Version wrapping a Yjs state vector.
128
+ *
129
+ * State vectors track the complete peer state — which operations from
130
+ * each client have been observed. This is the right abstraction for sync
131
+ * diffing: `exportSince(version)` uses the state vector to compute the
132
+ * minimal update payload via `Y.encodeStateAsUpdate(doc, sv)`.
133
+ *
134
+ * `serialize()` encodes to base64 for text-safe embedding.
135
+ * `compare()` decodes both state vectors and performs standard
136
+ * version-vector partial-order comparison over the client-clock maps.
137
+ */
138
+ declare class YjsVersion implements Version {
139
+ readonly sv: Uint8Array;
140
+ constructor(sv: Uint8Array);
141
+ /**
142
+ * Serialize the state vector to a base64 string.
143
+ *
144
+ * The encoding is: raw state vector bytes → base64.
145
+ * This is text-safe for embedding in HTML meta tags, URL parameters, etc.
146
+ */
147
+ serialize(): string;
148
+ /**
149
+ * Compare with another version using version-vector partial order.
150
+ *
151
+ * Delegates to the shared `versionVectorCompare` utility after decoding
152
+ * both state vectors via `Y.decodeStateVector()`.
153
+ *
154
+ * @throws If `other` is not a `YjsVersion`.
155
+ */
156
+ compare(other: Version): "behind" | "equal" | "ahead" | "concurrent";
157
+ /**
158
+ * Greatest lower bound (lattice meet) of two Yjs versions.
159
+ *
160
+ * Decodes both state vectors, computes the component-wise minimum
161
+ * via the shared `versionVectorMeet` utility, and encodes the result
162
+ * back to a Yjs state vector.
163
+ *
164
+ * @throws If `other` is not a `YjsVersion`.
165
+ */
166
+ meet(other: Version): YjsVersion;
167
+ /**
168
+ * Parse a serialized YjsVersion string back into a YjsVersion.
169
+ *
170
+ * The inverse of `serialize()`: base64 → `Uint8Array`.
171
+ */
172
+ static parse(serialized: string): YjsVersion;
173
+ }
174
+
265
175
  /**
266
176
  * Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
267
177
  *
@@ -281,35 +191,9 @@ declare function yjsReader(doc: Y.Doc, schema: Schema): Reader;
281
191
  * @param schema - The root schema for the document.
282
192
  */
283
193
  declare function createYjsSubstrate(doc: Y.Doc, schema: Schema): Substrate<YjsVersion>;
194
+ declare const yjsReplicaFactory: ReplicaFactory<YjsVersion>;
284
195
  declare const yjsSubstrateFactory: SubstrateFactory<YjsVersion>;
285
196
 
286
- /**
287
- * Returns the `Y.Doc` backing the given ref.
288
- *
289
- * This is the Yjs-specific escape hatch for accessing substrate-level
290
- * capabilities: raw Yjs API, y-prosemirror/y-codemirror bindings,
291
- * undo manager, awareness protocol, Yjs providers (y-websocket,
292
- * y-indexeddb, y-webrtc, Hocuspocus, Liveblocks), etc.
293
- *
294
- * Currently supports root document refs only. Child-level resolution
295
- * (e.g. `yjs(doc.title)` → `Y.Text`) is future work.
296
- *
297
- * @param ref - A root document ref backed by a Yjs substrate
298
- * @returns The `Y.Doc` backing the ref
299
- * @throws If the ref is not backed by a Yjs substrate
300
- *
301
- * @example
302
- * ```ts
303
- * import { yjs } from "@kyneta/yjs-schema"
304
- *
305
- * const doc = exchange.get("my-doc", TodoDoc)
306
- * const yjsDoc = yjs(doc)
307
- * console.log(yjsDoc.getMap("root").toJSON()) // raw state
308
- * console.log(yjsDoc.clientID) // client ID
309
- * ```
310
- */
311
- declare function yjs(ref: object): Doc;
312
-
313
197
  /**
314
198
  * Navigate one step deeper into the Yjs shared type tree.
315
199
  *
@@ -338,16 +222,4 @@ declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
338
222
  */
339
223
  declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
340
224
 
341
- /**
342
- * Collaborative text (CRDT). Produces `annotated("text")`.
343
- *
344
- * The annotation implies scalar string semantics for reads,
345
- * but the Yjs substrate provides collaborative editing (insert, delete)
346
- * via Y.Text.
347
- *
348
- * This is a convenience re-export so that `@kyneta/yjs-schema` users
349
- * don't need to import `LoroSchema` just for `text()`.
350
- */
351
- declare function text(): AnnotatedSchema<"text", undefined>;
352
-
353
- export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromEntirety, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, merge, resolveYjsType, stepIntoYjs, text, version, yjs, yjsReader, yjsSubstrateFactory };
225
+ export { type YjsCaps, type YjsNativeMap, YjsVersion, applyChangeToYjs, createYjsSubstrate, ensureContainers, eventsToOps, resolveYjsType, stepIntoYjs, yjs, yjsReader, yjsReplicaFactory, yjsSubstrateFactory };