@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/src/substrate.ts
CHANGED
|
@@ -8,25 +8,26 @@
|
|
|
8
8
|
// The event bridge contract: wrapping a Y.Doc in a kyneta substrate
|
|
9
9
|
// means subscribing to the kyneta doc observes ALL mutations to the
|
|
10
10
|
// underlying Y.Doc, regardless of source (local kyneta writes,
|
|
11
|
-
//
|
|
11
|
+
// merge, external Y.applyUpdate, external raw Yjs API mutations).
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
ChangeBase,
|
|
15
15
|
Path,
|
|
16
|
+
Reader,
|
|
17
|
+
Replica,
|
|
18
|
+
ReplicaFactory,
|
|
16
19
|
Schema as SchemaNode,
|
|
17
|
-
StoreReader,
|
|
18
20
|
Substrate,
|
|
19
21
|
SubstrateFactory,
|
|
20
22
|
SubstratePayload,
|
|
21
23
|
WritableContext,
|
|
22
24
|
} from "@kyneta/schema"
|
|
23
|
-
import { buildWritableContext, executeBatch } from "@kyneta/schema"
|
|
25
|
+
import { BACKING_DOC, buildWritableContext, executeBatch } from "@kyneta/schema"
|
|
24
26
|
import * as Y from "yjs"
|
|
25
27
|
import { applyChangeToYjs, eventsToOps } from "./change-mapping.js"
|
|
26
28
|
import { ensureContainers } from "./populate.js"
|
|
27
|
-
import {
|
|
29
|
+
import { yjsReader } from "./reader.js"
|
|
28
30
|
import { YjsVersion } from "./version.js"
|
|
29
|
-
import { registerYjsSubstrate } from "./yjs-escape.js"
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Origin tag — used to suppress echo from our own transactions
|
|
@@ -44,11 +45,11 @@ const KYNETA_ORIGIN = "kyneta-prepare"
|
|
|
44
45
|
* This is the "bring your own doc" entry point. The user creates and
|
|
45
46
|
* manages the Y.Doc (possibly via a Yjs provider); this function wraps
|
|
46
47
|
* it with a schema-aware overlay providing typed reads, writes,
|
|
47
|
-
* versioning, and export/
|
|
48
|
+
* versioning, and export/merge through the standard Substrate interface.
|
|
48
49
|
*
|
|
49
50
|
* **Event bridge contract:** A persistent `observeDeep` handler is
|
|
50
51
|
* registered on the root Y.Map at construction time. All non-kyneta
|
|
51
|
-
* mutations to the Y.Doc (
|
|
52
|
+
* mutations to the Y.Doc (merges, external local writes) are bridged
|
|
52
53
|
* to the kyneta changefeed. Subscribing to the kyneta doc observes all
|
|
53
54
|
* mutations regardless of source.
|
|
54
55
|
*
|
|
@@ -71,8 +72,8 @@ export function createYjsSubstrate(
|
|
|
71
72
|
// to be), and onFlush() skips transact/commit.
|
|
72
73
|
let inOurTransaction = false
|
|
73
74
|
|
|
74
|
-
// Stashed origin from
|
|
75
|
-
let
|
|
75
|
+
// Stashed origin from merge for the event bridge to pick up.
|
|
76
|
+
let pendingMergeOrigin: string | undefined
|
|
76
77
|
|
|
77
78
|
// Lazy-built WritableContext (same pattern as PlainSubstrate / LoroSubstrate).
|
|
78
79
|
let cachedCtx: WritableContext | undefined
|
|
@@ -80,13 +81,15 @@ export function createYjsSubstrate(
|
|
|
80
81
|
// The root Y.Map — all schema fields are children of this single map.
|
|
81
82
|
const rootMap = doc.getMap("root")
|
|
82
83
|
|
|
83
|
-
// The
|
|
84
|
-
const reader:
|
|
84
|
+
// The Reader — live view over the Yjs shared type tree.
|
|
85
|
+
const reader: Reader = yjsReader(doc, schema)
|
|
85
86
|
|
|
86
87
|
// --- Substrate object ---
|
|
87
88
|
|
|
88
|
-
const substrate
|
|
89
|
-
|
|
89
|
+
const substrate = {
|
|
90
|
+
[BACKING_DOC]: doc,
|
|
91
|
+
|
|
92
|
+
reader: reader,
|
|
90
93
|
|
|
91
94
|
prepare(path: Path, change: ChangeBase): void {
|
|
92
95
|
if (!inOurTransaction) {
|
|
@@ -129,8 +132,9 @@ export function createYjsSubstrate(
|
|
|
129
132
|
return new YjsVersion(Y.encodeStateVector(doc))
|
|
130
133
|
},
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
exportEntirety(): SubstratePayload {
|
|
133
136
|
return {
|
|
137
|
+
kind: "entirety",
|
|
134
138
|
encoding: "binary",
|
|
135
139
|
data: Y.encodeStateAsUpdate(doc),
|
|
136
140
|
}
|
|
@@ -139,27 +143,28 @@ export function createYjsSubstrate(
|
|
|
139
143
|
exportSince(since: YjsVersion): SubstratePayload | null {
|
|
140
144
|
try {
|
|
141
145
|
const bytes = Y.encodeStateAsUpdate(doc, since.sv)
|
|
142
|
-
return { encoding: "binary", data: bytes }
|
|
146
|
+
return { kind: "since", encoding: "binary", data: bytes }
|
|
143
147
|
} catch {
|
|
144
148
|
return null
|
|
145
149
|
}
|
|
146
150
|
},
|
|
147
151
|
|
|
148
|
-
|
|
152
|
+
merge(payload: SubstratePayload, origin?: string): void {
|
|
149
153
|
if (
|
|
150
154
|
payload.encoding !== "binary" ||
|
|
151
155
|
!(payload.data instanceof Uint8Array)
|
|
152
156
|
) {
|
|
153
157
|
throw new Error(
|
|
154
|
-
"YjsSubstrate.
|
|
158
|
+
"YjsSubstrate.merge expects binary-encoded payloads. " +
|
|
159
|
+
"If you recently switched CRDT backends, stale clients may be sending incompatible data.",
|
|
155
160
|
)
|
|
156
161
|
}
|
|
157
162
|
// Stash origin for the event bridge to pick up
|
|
158
|
-
|
|
163
|
+
pendingMergeOrigin = origin
|
|
159
164
|
try {
|
|
160
165
|
Y.applyUpdate(doc, payload.data, origin ?? "remote")
|
|
161
166
|
} finally {
|
|
162
|
-
|
|
167
|
+
pendingMergeOrigin = undefined
|
|
163
168
|
}
|
|
164
169
|
// That's it — the observeDeep handler bridges events to the
|
|
165
170
|
// changefeed via executeBatch.
|
|
@@ -180,13 +185,11 @@ export function createYjsSubstrate(
|
|
|
180
185
|
return
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
// Determine origin: prefer stashed kyneta origin (from
|
|
188
|
+
// Determine origin: prefer stashed kyneta origin (from merge),
|
|
184
189
|
// fall back to the transaction's origin if it's a string.
|
|
185
190
|
const origin =
|
|
186
|
-
|
|
187
|
-
(typeof transaction.origin === "string"
|
|
188
|
-
? transaction.origin
|
|
189
|
-
: undefined)
|
|
191
|
+
pendingMergeOrigin ??
|
|
192
|
+
(typeof transaction.origin === "string" ? transaction.origin : undefined)
|
|
190
193
|
|
|
191
194
|
// Lazily ensure the context is built
|
|
192
195
|
const ctx = substrate.context()
|
|
@@ -202,10 +205,7 @@ export function createYjsSubstrate(
|
|
|
202
205
|
}
|
|
203
206
|
})
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
registerYjsSubstrate(substrate, doc)
|
|
207
|
-
|
|
208
|
-
return substrate
|
|
208
|
+
return substrate as Substrate<YjsVersion>
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// ---------------------------------------------------------------------------
|
|
@@ -218,35 +218,134 @@ export function createYjsSubstrate(
|
|
|
218
218
|
* - `create(schema)` — creates a fresh Y.Doc with empty containers
|
|
219
219
|
* matching the schema structure. No seed data — initial content
|
|
220
220
|
* should be applied via `change()` after construction.
|
|
221
|
-
* - `
|
|
221
|
+
* - `fromEntirety(payload, schema)` — creates a Y.Doc from an entirety
|
|
222
222
|
* payload, returns a substrate.
|
|
223
223
|
* - `parseVersion(serialized)` — deserializes a YjsVersion.
|
|
224
224
|
*/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// yjsReplicaFactory — ReplicaFactory<YjsVersion>
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Schema-free replica factory for Yjs substrates.
|
|
231
|
+
*
|
|
232
|
+
* Constructs headless `Replica<YjsVersion>` instances backed by bare
|
|
233
|
+
* `Y.Doc`s — no schema walking, no container initialization, no
|
|
234
|
+
* Reader, no event bridge, no changefeed. Just the CRDT runtime
|
|
235
|
+
* with version tracking and export/merge.
|
|
236
|
+
*
|
|
237
|
+
* Used by conduit participants (stores, routing servers)
|
|
238
|
+
* that need to accumulate state, compute per-peer deltas, and compact
|
|
239
|
+
* storage without ever interpreting document fields.
|
|
240
|
+
*/
|
|
241
|
+
export function createYjsReplica(doc: Y.Doc): Replica<YjsVersion> {
|
|
242
|
+
return {
|
|
243
|
+
[BACKING_DOC]: doc,
|
|
244
|
+
|
|
245
|
+
version(): YjsVersion {
|
|
246
|
+
return new YjsVersion(Y.encodeStateVector(doc))
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
exportEntirety(): SubstratePayload {
|
|
250
|
+
return {
|
|
251
|
+
kind: "entirety",
|
|
252
|
+
encoding: "binary",
|
|
253
|
+
data: Y.encodeStateAsUpdate(doc),
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
exportSince(since: YjsVersion): SubstratePayload | null {
|
|
258
|
+
try {
|
|
259
|
+
const bytes = Y.encodeStateAsUpdate(doc, since.sv)
|
|
260
|
+
return { kind: "since", encoding: "binary", data: bytes }
|
|
261
|
+
} catch {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
merge(payload: SubstratePayload, _origin?: string): void {
|
|
267
|
+
if (
|
|
268
|
+
payload.encoding !== "binary" ||
|
|
269
|
+
!(payload.data instanceof Uint8Array)
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"YjsReplica.merge expects binary-encoded payloads. " +
|
|
273
|
+
"If you recently switched CRDT backends, stale clients may be sending incompatible data.",
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
Y.applyUpdate(doc, payload.data)
|
|
277
|
+
},
|
|
278
|
+
} as Replica<YjsVersion>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const yjsReplicaFactory: ReplicaFactory<YjsVersion> = {
|
|
282
|
+
replicaType: ["yjs", 1, 0] as const,
|
|
283
|
+
|
|
284
|
+
createEmpty(): Replica<YjsVersion> {
|
|
285
|
+
return createYjsReplica(new Y.Doc())
|
|
230
286
|
},
|
|
231
287
|
|
|
232
|
-
|
|
233
|
-
payload: SubstratePayload,
|
|
234
|
-
schema: SchemaNode,
|
|
235
|
-
): Substrate<YjsVersion> {
|
|
288
|
+
fromEntirety(payload: SubstratePayload): Replica<YjsVersion> {
|
|
236
289
|
if (
|
|
237
290
|
payload.encoding !== "binary" ||
|
|
238
291
|
!(payload.data instanceof Uint8Array)
|
|
239
292
|
) {
|
|
240
293
|
throw new Error(
|
|
241
|
-
"
|
|
294
|
+
"YjsReplicaFactory.fromEntirety only supports binary-encoded payloads",
|
|
242
295
|
)
|
|
243
296
|
}
|
|
244
297
|
const doc = new Y.Doc()
|
|
245
298
|
Y.applyUpdate(doc, payload.data)
|
|
299
|
+
return createYjsReplica(doc)
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
parseVersion(serialized: string): YjsVersion {
|
|
303
|
+
return YjsVersion.parse(serialized)
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// yjsSubstrateFactory — SubstrateFactory<YjsVersion>
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
export const yjsSubstrateFactory: SubstrateFactory<YjsVersion> = {
|
|
312
|
+
replica: yjsReplicaFactory,
|
|
313
|
+
|
|
314
|
+
createReplica(): Replica<YjsVersion> {
|
|
315
|
+
// Default random clientID — safe for hydration (no local writes).
|
|
316
|
+
return createYjsReplica(new Y.Doc())
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
upgrade(
|
|
320
|
+
replica: Replica<YjsVersion>,
|
|
321
|
+
schema: SchemaNode,
|
|
322
|
+
): Substrate<YjsVersion> {
|
|
323
|
+
const doc = (replica as any)[BACKING_DOC] as Y.Doc
|
|
324
|
+
// No identity injection for the standalone factory (no peerId).
|
|
325
|
+
// Conditional ensureContainers: skip fields that already exist
|
|
326
|
+
// from hydrated state.
|
|
327
|
+
ensureContainers(doc, schema, true)
|
|
246
328
|
return createYjsSubstrate(doc, schema)
|
|
247
329
|
},
|
|
248
330
|
|
|
331
|
+
create(schema: SchemaNode): Substrate<YjsVersion> {
|
|
332
|
+
// Fresh doc — unconditional ensureContainers (nothing to conflict with).
|
|
333
|
+
const doc = new Y.Doc()
|
|
334
|
+
ensureContainers(doc, schema)
|
|
335
|
+
return createYjsSubstrate(doc, schema)
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
fromEntirety(
|
|
339
|
+
payload: SubstratePayload,
|
|
340
|
+
schema: SchemaNode,
|
|
341
|
+
): Substrate<YjsVersion> {
|
|
342
|
+
// Two-phase path: createReplica → merge → upgrade
|
|
343
|
+
const replica = this.createReplica()
|
|
344
|
+
replica.merge(payload)
|
|
345
|
+
return this.upgrade(replica, schema)
|
|
346
|
+
},
|
|
347
|
+
|
|
249
348
|
parseVersion(serialized: string): YjsVersion {
|
|
250
349
|
return YjsVersion.parse(serialized)
|
|
251
350
|
},
|
|
252
|
-
}
|
|
351
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// sync — sync primitives for YjsSubstrate-backed documents.
|
|
2
2
|
//
|
|
3
|
-
// These functions provide version tracking,
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
3
|
+
// These functions provide version tracking, entirety export, and merge
|
|
4
|
+
// for documents created via `createYjsDoc` or
|
|
5
|
+
// `createYjsDocFromEntirety`. They discover the substrate via the
|
|
6
6
|
// module-scoped WeakMap in `create.ts`.
|
|
7
7
|
//
|
|
8
8
|
// Unlike PlainSubstrate's sync (which returns Op[] for deltas),
|
|
9
|
-
// YjsSubstrate's sync uses binary SubstratePayload for both
|
|
9
|
+
// YjsSubstrate's sync uses binary SubstratePayload for both entireties
|
|
10
10
|
// and deltas — these are Yjs's native state-as-update bytes.
|
|
11
11
|
|
|
12
12
|
import type { SubstratePayload } from "@kyneta/schema"
|
|
13
|
-
import { YjsVersion } from "./version.js"
|
|
14
13
|
import { getSubstrate } from "./create.js"
|
|
14
|
+
import type { YjsVersion } from "./version.js"
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// version — current YjsVersion
|
|
@@ -23,28 +23,28 @@ import { getSubstrate } from "./create.js"
|
|
|
23
23
|
* Use `.serialize()` to get a text-safe string for embedding in HTML
|
|
24
24
|
* meta tags, URL parameters, etc.
|
|
25
25
|
*
|
|
26
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
27
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
26
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
27
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
28
28
|
*/
|
|
29
29
|
export function version(doc: object): YjsVersion {
|
|
30
30
|
return getSubstrate(doc).version()
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
-
//
|
|
34
|
+
// exportEntirety — full state for reconstruction
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Export the full substrate
|
|
39
|
-
* reconstruct an equivalent document via `
|
|
38
|
+
* Export the full substrate entirety — sufficient for a new peer to
|
|
39
|
+
* reconstruct an equivalent document via `createYjsDocFromEntirety()`.
|
|
40
40
|
*
|
|
41
41
|
* Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
|
|
42
42
|
*
|
|
43
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
44
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
43
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
44
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
45
45
|
*/
|
|
46
|
-
export function
|
|
47
|
-
return getSubstrate(doc).
|
|
46
|
+
export function exportEntirety(doc: object): SubstratePayload {
|
|
47
|
+
return getSubstrate(doc).exportEntirety()
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
@@ -61,12 +61,12 @@ export function exportSnapshot(doc: object): SubstratePayload {
|
|
|
61
61
|
* const v0 = version(docA)
|
|
62
62
|
* change(docA, d => d.title.insert(0, "Hi"))
|
|
63
63
|
* const delta = exportSince(docA, v0)
|
|
64
|
-
*
|
|
64
|
+
* merge(docB, delta!)
|
|
65
65
|
* ```
|
|
66
66
|
*
|
|
67
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
67
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
68
68
|
* @param since - The version to diff from.
|
|
69
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
69
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
70
70
|
*/
|
|
71
71
|
export function exportSince(
|
|
72
72
|
doc: object,
|
|
@@ -76,32 +76,32 @@ export function exportSince(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ---------------------------------------------------------------------------
|
|
79
|
-
//
|
|
79
|
+
// merge — apply a delta from another peer
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Import a delta payload into a live document.
|
|
84
84
|
*
|
|
85
85
|
* The payload must have been produced by `exportSince()` or
|
|
86
|
-
* `
|
|
86
|
+
* `exportEntirety()` on a compatible document.
|
|
87
87
|
*
|
|
88
88
|
* After import, the changefeed fires for all subscribers — the event
|
|
89
89
|
* bridge handles this automatically.
|
|
90
90
|
*
|
|
91
91
|
* ```ts
|
|
92
92
|
* const delta = exportSince(docA, sinceVersion)
|
|
93
|
-
*
|
|
93
|
+
* merge(docB, delta!, "sync")
|
|
94
94
|
* ```
|
|
95
95
|
*
|
|
96
|
-
* @param doc - A document created by `createYjsDoc` or `
|
|
97
|
-
* @param payload - The delta or
|
|
96
|
+
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
97
|
+
* @param payload - The delta or entirety payload to merge.
|
|
98
98
|
* @param origin - Optional provenance tag for the changeset.
|
|
99
|
-
* @throws If `doc` was not created by `createYjsDoc` / `
|
|
99
|
+
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
100
100
|
*/
|
|
101
|
-
export function
|
|
101
|
+
export function merge(
|
|
102
102
|
doc: object,
|
|
103
103
|
payload: SubstratePayload,
|
|
104
104
|
origin?: string,
|
|
105
105
|
): void {
|
|
106
|
-
getSubstrate(doc).
|
|
107
|
-
}
|
|
106
|
+
getSubstrate(doc).merge(payload, origin)
|
|
107
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -85,9 +85,7 @@ export class YjsVersion implements Version {
|
|
|
85
85
|
*/
|
|
86
86
|
compare(other: Version): "behind" | "equal" | "ahead" | "concurrent" {
|
|
87
87
|
if (!(other instanceof YjsVersion)) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"YjsVersion can only be compared with another YjsVersion",
|
|
90
|
-
)
|
|
88
|
+
throw new Error("YjsVersion can only be compared with another YjsVersion")
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
const thisMap = decodeStateVector(this.sv)
|
|
@@ -135,4 +133,4 @@ export class YjsVersion implements Version {
|
|
|
135
133
|
const bytes = base64ToUint8Array(serialized)
|
|
136
134
|
return new YjsVersion(bytes)
|
|
137
135
|
}
|
|
138
|
-
}
|
|
136
|
+
}
|
package/src/yjs-escape.ts
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
// `yjs(ref)` returns the `Y.Doc` backing a root document ref.
|
|
5
5
|
//
|
|
6
|
-
// The
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// to get the substrate, then looks up the Y.Doc.
|
|
6
|
+
// The substrate exposes its backing Y.Doc via the `BACKING_DOC` symbol
|
|
7
|
+
// (from `@kyneta/schema`). The `yjs()` function uses `unwrap()` to get
|
|
8
|
+
// the substrate, then reads `[BACKING_DOC]` to get the Y.Doc.
|
|
10
9
|
//
|
|
11
10
|
// This two-step approach (ref → substrate → Y.Doc) avoids duplicating
|
|
12
11
|
// the ref-tracking WeakMap and composes cleanly with the general
|
|
13
12
|
// `unwrap()` escape hatch.
|
|
14
13
|
//
|
|
14
|
+
// Context: jj:smmulzkm (BACKING_DOC replaces WeakMap + registerYjsSubstrate)
|
|
15
|
+
//
|
|
15
16
|
// Usage:
|
|
16
17
|
// import { yjs } from "@kyneta/yjs-schema"
|
|
17
18
|
//
|
|
@@ -19,33 +20,8 @@
|
|
|
19
20
|
// const yjsDoc = yjs(doc) // Y.Doc
|
|
20
21
|
// yjsDoc.getMap("root").toJSON() // raw Yjs inspection
|
|
21
22
|
|
|
23
|
+
import { BACKING_DOC, unwrap } from "@kyneta/schema"
|
|
22
24
|
import type { Doc as YDoc } from "yjs"
|
|
23
|
-
import type { Substrate } from "@kyneta/schema"
|
|
24
|
-
import { unwrap } from "@kyneta/schema"
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Substrate → Y.Doc mapping
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
const substrateToYjsDoc = new WeakMap<Substrate<any>, YDoc>()
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// registerYjsSubstrate — called during substrate creation
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Register the Y.Doc backing a Yjs substrate.
|
|
38
|
-
*
|
|
39
|
-
* Called by `createYjsSubstrate()` and by `bindYjs`'s factory builder
|
|
40
|
-
* to enable the `yjs()` escape hatch. Must be called once per substrate
|
|
41
|
-
* at construction time.
|
|
42
|
-
*/
|
|
43
|
-
export function registerYjsSubstrate(
|
|
44
|
-
substrate: Substrate<any>,
|
|
45
|
-
doc: YDoc,
|
|
46
|
-
): void {
|
|
47
|
-
substrateToYjsDoc.set(substrate, doc)
|
|
48
|
-
}
|
|
49
25
|
|
|
50
26
|
// ---------------------------------------------------------------------------
|
|
51
27
|
// yjs — Yjs-specific escape hatch
|
|
@@ -77,7 +53,7 @@ export function registerYjsSubstrate(
|
|
|
77
53
|
* ```
|
|
78
54
|
*/
|
|
79
55
|
export function yjs(ref: object): YDoc {
|
|
80
|
-
let substrate:
|
|
56
|
+
let substrate: any
|
|
81
57
|
try {
|
|
82
58
|
substrate = unwrap(ref)
|
|
83
59
|
} catch {
|
|
@@ -88,13 +64,21 @@ export function yjs(ref: object): YDoc {
|
|
|
88
64
|
)
|
|
89
65
|
}
|
|
90
66
|
|
|
91
|
-
const doc =
|
|
92
|
-
|
|
67
|
+
const doc = substrate[BACKING_DOC]
|
|
68
|
+
// Duck-type check: Y.Doc has getMap, encodeStateVector-compatible API,
|
|
69
|
+
// and a numeric clientID. A PlainState (plain object) or LoroDoc would
|
|
70
|
+
// not have getMap as a function.
|
|
71
|
+
if (
|
|
72
|
+
!doc ||
|
|
73
|
+
typeof doc !== "object" ||
|
|
74
|
+
typeof (doc as any).getMap !== "function" ||
|
|
75
|
+
typeof (doc as any).clientID !== "number"
|
|
76
|
+
) {
|
|
93
77
|
throw new Error(
|
|
94
78
|
"yjs() requires a ref backed by a Yjs substrate. " +
|
|
95
79
|
"The ref has a substrate but it is not a Yjs substrate. " +
|
|
96
80
|
"Use a doc created with a bindYjs() schema or createYjsDoc().",
|
|
97
81
|
)
|
|
98
82
|
}
|
|
99
|
-
return doc
|
|
100
|
-
}
|
|
83
|
+
return doc as YDoc
|
|
84
|
+
}
|
package/src/yjs-resolve.ts
CHANGED
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
// Using a single root Y.Map enables one `observeDeep` call that
|
|
15
15
|
// captures all mutations with correct relative paths.
|
|
16
16
|
|
|
17
|
+
import type { Path, Schema as SchemaNode, Segment } from "@kyneta/schema"
|
|
17
18
|
import { advanceSchema } from "@kyneta/schema"
|
|
18
|
-
import type { Path, Segment } from "@kyneta/schema"
|
|
19
|
-
import type { Schema as SchemaNode } from "@kyneta/schema"
|
|
20
19
|
import * as Y from "yjs"
|
|
21
20
|
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
@@ -35,10 +34,7 @@ import * as Y from "yjs"
|
|
|
35
34
|
* @param current - The current position (a Yjs shared type or plain value)
|
|
36
35
|
* @param segment - The path segment to follow
|
|
37
36
|
*/
|
|
38
|
-
export function stepIntoYjs(
|
|
39
|
-
current: unknown,
|
|
40
|
-
segment: Segment,
|
|
41
|
-
): unknown {
|
|
37
|
+
export function stepIntoYjs(current: unknown, segment: Segment): unknown {
|
|
42
38
|
const resolved = segment.resolve()
|
|
43
39
|
|
|
44
40
|
if (current instanceof Y.Map) {
|
|
@@ -50,9 +46,7 @@ export function stepIntoYjs(
|
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
if (current instanceof Y.Text) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`yjs-resolve: cannot step into Y.Text`,
|
|
55
|
-
)
|
|
49
|
+
throw new Error(`yjs-resolve: cannot step into Y.Text`)
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
// Plain value — terminal, cannot step further
|
|
@@ -105,4 +99,4 @@ export function resolveYjsType(
|
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
return current
|
|
108
|
-
}
|
|
102
|
+
}
|