@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/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
  *
@@ -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
- }