@kyneta/yjs-schema 1.1.0 → 1.2.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 +34 -74
- package/dist/index.js +181 -132
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +333 -0
- package/src/__tests__/bind-yjs.test.ts +38 -40
- package/src/__tests__/create.test.ts +10 -11
- package/src/__tests__/reader.test.ts +38 -61
- package/src/__tests__/record-text-spike.test.ts +9 -10
- package/src/__tests__/structural-merge.test.ts +18 -18
- package/src/__tests__/substrate.test.ts +18 -21
- package/src/__tests__/version.test.ts +75 -0
- package/src/bind-yjs.ts +72 -42
- package/src/change-mapping.ts +46 -55
- package/src/create.ts +2 -2
- package/src/index.ts +12 -25
- package/src/populate.ts +50 -83
- package/src/substrate.ts +52 -7
- package/src/version.ts +55 -0
- package/src/yjs-resolve.ts +1 -11
- package/src/yjs-escape.ts +0 -84
package/src/version.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// decoded `Map<number, number>` (clientID → clock) maps ourselves.
|
|
13
13
|
|
|
14
14
|
import type { Version } from "@kyneta/schema"
|
|
15
|
+
import { versionVectorMeet } from "@kyneta/schema"
|
|
15
16
|
import { decodeStateVector } from "yjs"
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -35,6 +36,39 @@ function base64ToUint8Array(base64: string): Uint8Array {
|
|
|
35
36
|
return bytes
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// State vector encoding — manual varint (unsigned LEB128)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encode a state vector map to Yjs's binary state vector format.
|
|
45
|
+
*
|
|
46
|
+
* Yjs does not export `encodeStateVector(map)` — only `Y.encodeStateVector(doc)`
|
|
47
|
+
* which requires a full doc. This implements the same binary format directly:
|
|
48
|
+
* `[entryCount: varint, (clientId: varint, clock: varint)*]`
|
|
49
|
+
*
|
|
50
|
+
* Each value is encoded as an unsigned LEB128 varint.
|
|
51
|
+
*/
|
|
52
|
+
function encodeStateVector(map: Map<number, number>): Uint8Array {
|
|
53
|
+
const bytes: number[] = []
|
|
54
|
+
|
|
55
|
+
function writeVarUint(value: number): void {
|
|
56
|
+
while (value > 0x7f) {
|
|
57
|
+
bytes.push((value & 0x7f) | 0x80)
|
|
58
|
+
value >>>= 7
|
|
59
|
+
}
|
|
60
|
+
bytes.push(value & 0x7f)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writeVarUint(map.size)
|
|
64
|
+
for (const [clientId, clock] of map) {
|
|
65
|
+
writeVarUint(clientId)
|
|
66
|
+
writeVarUint(clock)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new Uint8Array(bytes)
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
// ---------------------------------------------------------------------------
|
|
39
73
|
// YjsVersion
|
|
40
74
|
// ---------------------------------------------------------------------------
|
|
@@ -121,6 +155,27 @@ export class YjsVersion implements Version {
|
|
|
121
155
|
return "equal"
|
|
122
156
|
}
|
|
123
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Greatest lower bound (lattice meet) of two Yjs versions.
|
|
160
|
+
*
|
|
161
|
+
* Decodes both state vectors, computes the component-wise minimum
|
|
162
|
+
* via the shared `versionVectorMeet` utility, and encodes the result
|
|
163
|
+
* back to a Yjs state vector.
|
|
164
|
+
*
|
|
165
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
166
|
+
*/
|
|
167
|
+
meet(other: Version): YjsVersion {
|
|
168
|
+
if (!(other instanceof YjsVersion)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"YjsVersion can only be meet'd with another YjsVersion",
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
const thisMap = decodeStateVector(this.sv)
|
|
174
|
+
const otherMap = decodeStateVector(other.sv)
|
|
175
|
+
const result = versionVectorMeet(thisMap, otherMap)
|
|
176
|
+
return new YjsVersion(encodeStateVector(result))
|
|
177
|
+
}
|
|
178
|
+
|
|
124
179
|
/**
|
|
125
180
|
* Parse a serialized YjsVersion string back into a YjsVersion.
|
|
126
181
|
*
|
package/src/yjs-resolve.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// captures all mutations with correct relative paths.
|
|
16
16
|
|
|
17
17
|
import type { Path, Schema as SchemaNode, Segment } from "@kyneta/schema"
|
|
18
|
-
import { advanceSchema } from "@kyneta/schema"
|
|
18
|
+
import { advanceSchema, KIND } from "@kyneta/schema"
|
|
19
19
|
import * as Y from "yjs"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -78,16 +78,6 @@ export function resolveYjsType(
|
|
|
78
78
|
let current: unknown = rootMap
|
|
79
79
|
let schema = rootSchema
|
|
80
80
|
|
|
81
|
-
// Unwrap the root annotation (e.g. annotated("doc", product))
|
|
82
|
-
// to reach the product schema whose fields are the root map's children.
|
|
83
|
-
let rootProduct = rootSchema
|
|
84
|
-
while (
|
|
85
|
-
rootProduct._kind === "annotated" &&
|
|
86
|
-
rootProduct.schema !== undefined
|
|
87
|
-
) {
|
|
88
|
-
rootProduct = rootProduct.schema
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
for (let i = 0; i < path.length; i++) {
|
|
92
82
|
const seg = path.segments[i]!
|
|
93
83
|
const nextSchema = advanceSchema(schema, seg)
|
package/src/yjs-escape.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// yjs-escape — Yjs-specific escape hatch for accessing the Y.Doc
|
|
2
|
-
// backing a ref.
|
|
3
|
-
//
|
|
4
|
-
// `yjs(ref)` returns the `Y.Doc` backing a root document ref.
|
|
5
|
-
//
|
|
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.
|
|
9
|
-
//
|
|
10
|
-
// This two-step approach (ref → substrate → Y.Doc) avoids duplicating
|
|
11
|
-
// the ref-tracking WeakMap and composes cleanly with the general
|
|
12
|
-
// `unwrap()` escape hatch.
|
|
13
|
-
//
|
|
14
|
-
// Context: jj:smmulzkm (BACKING_DOC replaces WeakMap + registerYjsSubstrate)
|
|
15
|
-
//
|
|
16
|
-
// Usage:
|
|
17
|
-
// import { yjs } from "@kyneta/yjs-schema"
|
|
18
|
-
//
|
|
19
|
-
// const doc = exchange.get("my-doc", TodoDoc)
|
|
20
|
-
// const yjsDoc = yjs(doc) // Y.Doc
|
|
21
|
-
// yjsDoc.getMap("root").toJSON() // raw Yjs inspection
|
|
22
|
-
|
|
23
|
-
import { BACKING_DOC, unwrap } from "@kyneta/schema"
|
|
24
|
-
import type { Doc as YDoc } from "yjs"
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// yjs — Yjs-specific escape hatch
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Returns the `Y.Doc` backing the given ref.
|
|
32
|
-
*
|
|
33
|
-
* This is the Yjs-specific escape hatch for accessing substrate-level
|
|
34
|
-
* capabilities: raw Yjs API, y-prosemirror/y-codemirror bindings,
|
|
35
|
-
* undo manager, awareness protocol, Yjs providers (y-websocket,
|
|
36
|
-
* y-indexeddb, y-webrtc, Hocuspocus, Liveblocks), etc.
|
|
37
|
-
*
|
|
38
|
-
* Currently supports root document refs only. Child-level resolution
|
|
39
|
-
* (e.g. `yjs(doc.title)` → `Y.Text`) is future work.
|
|
40
|
-
*
|
|
41
|
-
* @param ref - A root document ref backed by a Yjs substrate
|
|
42
|
-
* @returns The `Y.Doc` backing the ref
|
|
43
|
-
* @throws If the ref is not backed by a Yjs substrate
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* import { yjs } from "@kyneta/yjs-schema"
|
|
48
|
-
*
|
|
49
|
-
* const doc = exchange.get("my-doc", TodoDoc)
|
|
50
|
-
* const yjsDoc = yjs(doc)
|
|
51
|
-
* console.log(yjsDoc.getMap("root").toJSON()) // raw state
|
|
52
|
-
* console.log(yjsDoc.clientID) // client ID
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export function yjs(ref: object): YDoc {
|
|
56
|
-
let substrate: any
|
|
57
|
-
try {
|
|
58
|
-
substrate = unwrap(ref)
|
|
59
|
-
} catch {
|
|
60
|
-
throw new Error(
|
|
61
|
-
"yjs() requires a ref backed by a Yjs substrate. " +
|
|
62
|
-
"Use a doc created by exchange.get() with a bindYjs() schema, " +
|
|
63
|
-
"or by createYjsDoc().",
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
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
|
-
) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
"yjs() requires a ref backed by a Yjs substrate. " +
|
|
79
|
-
"The ref has a substrate but it is not a Yjs substrate. " +
|
|
80
|
-
"Use a doc created with a bindYjs() schema or createYjsDoc().",
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
return doc as YDoc
|
|
84
|
-
}
|