@kyneta/yjs-schema 1.0.0 → 1.1.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/dist/index.d.ts +110 -108
- package/dist/index.js +171 -109
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/bind-yjs.test.ts +19 -19
- package/src/__tests__/create.test.ts +61 -51
- package/src/__tests__/{store-reader.test.ts → reader.test.ts} +30 -33
- package/src/__tests__/record-text-spike.test.ts +29 -21
- package/src/__tests__/structural-merge.test.ts +362 -0
- package/src/__tests__/substrate.test.ts +48 -64
- package/src/__tests__/version.test.ts +7 -16
- package/src/bind-yjs.ts +46 -25
- package/src/change-mapping.ts +20 -35
- package/src/create.ts +32 -27
- package/src/index.ts +24 -30
- package/src/populate.ts +42 -14
- package/src/{store-reader.ts → reader.ts} +7 -12
- package/src/substrate.ts +139 -40
- package/src/sync.ts +26 -26
- package/src/version.ts +2 -4
- package/src/yjs-escape.ts +19 -35
- package/src/yjs-resolve.ts +4 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Version, Schema, Ref, SubstratePayload,
|
|
1
|
+
import { Version, Schema, Ref, SubstratePayload, BoundSchema, Path, ChangeBase, Op, Reader, Substrate, SubstrateFactory, Segment, AnnotatedSchema } from '@kyneta/schema';
|
|
2
2
|
export { Changeset, Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
|
|
3
3
|
import * as Y from 'yjs';
|
|
4
4
|
import { Doc } from 'yjs';
|
|
@@ -74,7 +74,7 @@ declare class YjsVersion implements Version {
|
|
|
74
74
|
* CRDT collaboration support.
|
|
75
75
|
*
|
|
76
76
|
* The returned ref observes **all** mutations to the underlying Y.Doc,
|
|
77
|
-
* regardless of source (local kyneta writes,
|
|
77
|
+
* regardless of source (local kyneta writes, merge, external
|
|
78
78
|
* `Y.applyUpdate()`, external raw Yjs API mutations).
|
|
79
79
|
*
|
|
80
80
|
* @param schema - The schema describing the document structure.
|
|
@@ -83,21 +83,21 @@ declare class YjsVersion implements Version {
|
|
|
83
83
|
*/
|
|
84
84
|
type CreateYjsDoc = <S extends Schema>(schema: S, doc?: Y.Doc) => Ref<S>;
|
|
85
85
|
declare const createYjsDoc: CreateYjsDoc;
|
|
86
|
-
type
|
|
86
|
+
type CreateYjsDocFromEntirety = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
|
|
87
87
|
/**
|
|
88
|
-
* Reconstruct a live Yjs-backed document from a substrate
|
|
88
|
+
* Reconstruct a live Yjs-backed document from a substrate entirety payload.
|
|
89
89
|
*
|
|
90
|
-
* The payload must have been produced by `
|
|
90
|
+
* The payload must have been produced by `exportEntirety()` on a
|
|
91
91
|
* compatible document. This is the entry point for SSR hydration
|
|
92
92
|
* and reconnection past log compaction.
|
|
93
93
|
*
|
|
94
94
|
* ```ts
|
|
95
|
-
* const payload =
|
|
96
|
-
* const docB =
|
|
95
|
+
* const payload = exportEntirety(docA)
|
|
96
|
+
* const docB = createYjsDocFromEntirety(MySchema, payload)
|
|
97
97
|
* // docB has the same state as docA at the time of export
|
|
98
98
|
* ```
|
|
99
99
|
*/
|
|
100
|
-
declare const
|
|
100
|
+
declare const createYjsDocFromEntirety: CreateYjsDocFromEntirety;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Current version as a `YjsVersion` (wrapping a Yjs state vector).
|
|
@@ -105,20 +105,20 @@ declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
|
|
|
105
105
|
* Use `.serialize()` to get a text-safe string for embedding in HTML
|
|
106
106
|
* meta tags, URL parameters, etc.
|
|
107
107
|
*
|
|
108
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
109
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
108
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
109
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
110
110
|
*/
|
|
111
111
|
declare function version(doc: object): YjsVersion;
|
|
112
112
|
/**
|
|
113
|
-
* Export the full substrate
|
|
114
|
-
* reconstruct an equivalent document via `
|
|
113
|
+
* Export the full substrate entirety — sufficient for a new peer to
|
|
114
|
+
* reconstruct an equivalent document via `createYjsDocFromEntirety()`.
|
|
115
115
|
*
|
|
116
116
|
* Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
|
|
117
117
|
*
|
|
118
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
119
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
118
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
119
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
120
120
|
*/
|
|
121
|
-
declare function
|
|
121
|
+
declare function exportEntirety(doc: object): SubstratePayload;
|
|
122
122
|
/**
|
|
123
123
|
* Export a delta payload containing all changes since the given version.
|
|
124
124
|
*
|
|
@@ -129,78 +129,65 @@ declare function exportSnapshot(doc: object): SubstratePayload;
|
|
|
129
129
|
* const v0 = version(docA)
|
|
130
130
|
* change(docA, d => d.title.insert(0, "Hi"))
|
|
131
131
|
* const delta = exportSince(docA, v0)
|
|
132
|
-
*
|
|
132
|
+
* merge(docB, delta!)
|
|
133
133
|
* ```
|
|
134
134
|
*
|
|
135
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
135
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
136
136
|
* @param since - The version to diff from.
|
|
137
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
137
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
138
138
|
*/
|
|
139
139
|
declare function exportSince(doc: object, since: YjsVersion): SubstratePayload | null;
|
|
140
140
|
/**
|
|
141
141
|
* Import a delta payload into a live document.
|
|
142
142
|
*
|
|
143
143
|
* The payload must have been produced by `exportSince()` or
|
|
144
|
-
* `
|
|
144
|
+
* `exportEntirety()` on a compatible document.
|
|
145
145
|
*
|
|
146
146
|
* After import, the changefeed fires for all subscribers — the event
|
|
147
147
|
* bridge handles this automatically.
|
|
148
148
|
*
|
|
149
149
|
* ```ts
|
|
150
150
|
* const delta = exportSince(docA, sinceVersion)
|
|
151
|
-
*
|
|
151
|
+
* merge(docB, delta!, "sync")
|
|
152
152
|
* ```
|
|
153
153
|
*
|
|
154
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
155
|
-
* @param payload - The delta or
|
|
154
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
155
|
+
* @param payload - The delta or entirety payload to merge.
|
|
156
156
|
* @param origin - Optional provenance tag for the changeset.
|
|
157
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
157
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
158
158
|
*/
|
|
159
|
-
declare function
|
|
159
|
+
declare function merge(doc: object, payload: SubstratePayload, origin?: string): void;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
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.
|
|
162
|
+
* Bind a schema to the Yjs CRDT substrate with causal merge strategy.
|
|
179
163
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* - Plain value → terminal (return `undefined`)
|
|
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.
|
|
185
168
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Resolve a Yjs shared type (or plain value) at the given path.
|
|
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.
|
|
192
173
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* import { bindYjs } from "@kyneta/yjs-schema"
|
|
177
|
+
* import { Schema } from "@kyneta/schema"
|
|
195
178
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
+
* }))
|
|
198
186
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* @param path - The path to resolve
|
|
187
|
+
* const doc = exchange.get("my-todos", TodoDoc)
|
|
188
|
+
* ```
|
|
202
189
|
*/
|
|
203
|
-
declare function
|
|
190
|
+
declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
|
|
204
191
|
|
|
205
192
|
/**
|
|
206
193
|
* Apply a kyneta Change to the Yjs shared type tree imperatively.
|
|
@@ -238,14 +225,42 @@ declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
|
|
|
238
225
|
* schema, and creates empty containers for each field within a single
|
|
239
226
|
* `doc.transact()` call for atomicity.
|
|
240
227
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
228
|
+
* When `conditional` is true, fields that already exist in the root map
|
|
229
|
+
* are skipped. This is the correct mode after hydration — containers
|
|
230
|
+
* present from stored state must not be overwritten (each `rootMap.set()`
|
|
231
|
+
* is a CRDT write that advances the version vector and may conflict
|
|
232
|
+
* with stored operations).
|
|
233
|
+
*
|
|
234
|
+
* When `conditional` is false (default), all fields are created
|
|
235
|
+
* unconditionally. This is the correct mode for fresh documents.
|
|
236
|
+
*
|
|
237
|
+
* **Structural identity:** This function temporarily sets `doc.clientID`
|
|
238
|
+
* to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
|
|
239
|
+
* then restores the caller's clientID. This produces byte-identical
|
|
240
|
+
* structural ops across all peers, enabling Yjs deduplication on merge.
|
|
244
241
|
*
|
|
245
242
|
* @param doc - The Y.Doc to prepare
|
|
246
243
|
* @param schema - The root document schema (typically annotated("doc", product))
|
|
244
|
+
* @param conditional - If true, skip fields that already exist in the root map.
|
|
245
|
+
* Context: jj:smmulzkm (two-phase substrate construction)
|
|
246
|
+
*/
|
|
247
|
+
declare function ensureContainers(doc: Y.Doc, schema: Schema, conditional?: boolean): void;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Creates a Reader that navigates the Yjs shared type tree live,
|
|
251
|
+
* using the schema as a type witness to determine navigation at each
|
|
252
|
+
* path segment.
|
|
253
|
+
*
|
|
254
|
+
* The reader is a live view — mutations to the underlying Y.Doc
|
|
255
|
+
* (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
|
|
256
|
+
* visible through the reader.
|
|
257
|
+
*
|
|
258
|
+
* Internally obtains the root map via `doc.getMap("root")`.
|
|
259
|
+
*
|
|
260
|
+
* @param doc - The Y.Doc to read from.
|
|
261
|
+
* @param schema - The root schema for the document.
|
|
247
262
|
*/
|
|
248
|
-
declare function
|
|
263
|
+
declare function yjsReader(doc: Y.Doc, schema: Schema): Reader;
|
|
249
264
|
|
|
250
265
|
/**
|
|
251
266
|
* Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
|
|
@@ -253,11 +268,11 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
|
|
|
253
268
|
* This is the "bring your own doc" entry point. The user creates and
|
|
254
269
|
* manages the Y.Doc (possibly via a Yjs provider); this function wraps
|
|
255
270
|
* it with a schema-aware overlay providing typed reads, writes,
|
|
256
|
-
* versioning, and export/
|
|
271
|
+
* versioning, and export/merge through the standard Substrate interface.
|
|
257
272
|
*
|
|
258
273
|
* **Event bridge contract:** A persistent `observeDeep` handler is
|
|
259
274
|
* registered on the root Y.Map at construction time. All non-kyneta
|
|
260
|
-
* mutations to the Y.Doc (
|
|
275
|
+
* mutations to the Y.Doc (merges, external local writes) are bridged
|
|
261
276
|
* to the kyneta changefeed. Subscribing to the kyneta doc observes all
|
|
262
277
|
* mutations regardless of source.
|
|
263
278
|
*
|
|
@@ -266,49 +281,8 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
|
|
|
266
281
|
* @param schema - The root schema for the document.
|
|
267
282
|
*/
|
|
268
283
|
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
284
|
declare const yjsSubstrateFactory: SubstrateFactory<YjsVersion>;
|
|
280
285
|
|
|
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
286
|
/**
|
|
313
287
|
* Returns the `Y.Doc` backing the given ref.
|
|
314
288
|
*
|
|
@@ -336,6 +310,34 @@ declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
|
|
|
336
310
|
*/
|
|
337
311
|
declare function yjs(ref: object): Doc;
|
|
338
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Navigate one step deeper into the Yjs shared type tree.
|
|
315
|
+
*
|
|
316
|
+
* Uses `instanceof` for runtime type discrimination:
|
|
317
|
+
* - `Y.Map` → `.get(key)`
|
|
318
|
+
* - `Y.Array` → `.get(index)`
|
|
319
|
+
* - `Y.Text` → terminal (cannot step further)
|
|
320
|
+
* - Plain value → terminal (return `undefined`)
|
|
321
|
+
*
|
|
322
|
+
* @param current - The current position (a Yjs shared type or plain value)
|
|
323
|
+
* @param segment - The path segment to follow
|
|
324
|
+
*/
|
|
325
|
+
declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
|
|
326
|
+
/**
|
|
327
|
+
* Resolve a Yjs shared type (or plain value) at the given path.
|
|
328
|
+
*
|
|
329
|
+
* Left-folds over path segments using `advanceSchema` for pure schema
|
|
330
|
+
* descent and `stepIntoYjs` for Yjs-specific navigation.
|
|
331
|
+
*
|
|
332
|
+
* Returns the Yjs shared type or plain value at the terminal position.
|
|
333
|
+
* For an empty path, returns the root map itself.
|
|
334
|
+
*
|
|
335
|
+
* @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
|
|
336
|
+
* @param rootSchema - The root document schema
|
|
337
|
+
* @param path - The path to resolve
|
|
338
|
+
*/
|
|
339
|
+
declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
|
|
340
|
+
|
|
339
341
|
/**
|
|
340
342
|
* Collaborative text (CRDT). Produces `annotated("text")`.
|
|
341
343
|
*
|
|
@@ -348,4 +350,4 @@ declare function yjs(ref: object): Doc;
|
|
|
348
350
|
*/
|
|
349
351
|
declare function text(): AnnotatedSchema<"text", undefined>;
|
|
350
352
|
|
|
351
|
-
export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc,
|
|
353
|
+
export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromEntirety, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, merge, resolveYjsType, stepIntoYjs, text, version, yjs, yjsReader, yjsSubstrateFactory };
|