@kyneta/yjs-schema 1.6.1 → 1.8.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.
@@ -1,35 +1,29 @@
1
1
  // yjs-resolve — Yjs-specific path resolution.
2
2
  //
3
- // Implements stepIntoYjs and resolveYjsType for schema-guided
4
- // navigation of the Yjs shared type tree.
5
- //
6
- // resolveYjsType is a left-fold over path segments, accumulating
7
- // (currentType, currentSchema) at each step. This mirrors how
8
- // resolveContainer works for Loro — but uses `instanceof` for
9
- // runtime type discrimination instead of Loro's `.kind()` method.
3
+ // `stepIntoYjs` is the per-step substrate dispatch; `resolveYjsType`
4
+ // applies the core `foldPath` primitive (from `@kyneta/schema`) around
5
+ // it. The semantic invariants of the fold — identity-keying at
6
+ // product-field boundaries, sum-boundary short-circuit live in
7
+ // `fold-path.ts`, not here.
10
8
  //
11
9
  // Root container strategy: All schema fields are children of a single
12
10
  // root `Y.Map` obtained via `doc.getMap("root")`. This root map holds
13
11
  // shared types (Y.Text, Y.Array, Y.Map) and plain values uniformly.
14
12
  // Using a single root Y.Map enables one `observeDeep` call that
15
13
  // captures all mutations with correct relative paths.
16
- //
17
- // Identity-keying: when a SchemaBinding is provided, every product-field
18
- // boundary uses the identity hash (from binding.forward) instead of the
19
- // field name as the Y.Map key. The binding is threaded through
20
- // resolveYjsType and stepIntoYjs.
21
14
 
22
- import type {
23
- Path,
24
- SchemaBinding,
25
- Schema as SchemaNode,
26
- Segment,
15
+ import {
16
+ foldPath,
17
+ type Path,
18
+ type PathFoldResult,
19
+ type PathStepper,
20
+ type SchemaBinding,
21
+ type Schema as SchemaNode,
27
22
  } from "@kyneta/schema"
28
- import { advanceSchema, KIND } from "@kyneta/schema"
29
23
  import * as Y from "yjs"
30
24
 
31
25
  // ---------------------------------------------------------------------------
32
- // stepIntoYjs — single step of the fold
26
+ // stepIntoYjs — per-step substrate dispatch (PathStepper for Yjs)
33
27
  // ---------------------------------------------------------------------------
34
28
 
35
29
  /**
@@ -41,15 +35,16 @@ import * as Y from "yjs"
41
35
  * - `Y.Text` → terminal (cannot step further)
42
36
  * - Plain value → terminal (return `undefined`)
43
37
  *
44
- * @param current - The current position (a Yjs shared type or plain value)
45
- * @param segment - The path segment to follow
46
- * @param identity - Optional identity hash to use instead of the segment's resolved value
38
+ * `_nextSchema` is part of the `PathStepper` contract for Loro's root
39
+ * dispatch but is unused here Yjs's `instanceof` dispatch doesn't
40
+ * need to look ahead at the next schema kind.
47
41
  */
48
- export function stepIntoYjs(
49
- current: unknown,
50
- segment: Segment,
51
- identity?: string,
52
- ): unknown {
42
+ export const stepIntoYjs: PathStepper = (
43
+ current,
44
+ _nextSchema,
45
+ segment,
46
+ identity,
47
+ ) => {
53
48
  const resolved = segment.resolve()
54
49
 
55
50
  if (current instanceof Y.Map) {
@@ -69,85 +64,25 @@ export function stepIntoYjs(
69
64
  }
70
65
 
71
66
  // ---------------------------------------------------------------------------
72
- // resolveYjsType — full path resolution via left-fold
67
+ // resolveYjsType — full path resolution via foldPath
73
68
  // ---------------------------------------------------------------------------
74
69
 
75
- /**
76
- * Result of resolving a Yjs shared type at a path.
77
- *
78
- * Includes both the resolved Yjs value and the schema at that position,
79
- * enabling callers to distinguish between schema kinds that map to the
80
- * same Yjs type (e.g. "text" vs "richtext" both use Y.Text).
81
- */
82
- export interface ResolvedYjs {
83
- readonly resolved: unknown
84
- readonly schema: SchemaNode
85
- }
86
-
87
70
  /**
88
71
  * Resolve a Yjs shared type (or plain value) at the given path.
89
72
  *
90
- * Left-folds over path segments using `advanceSchema` for pure schema
91
- * descent and `stepIntoYjs` for Yjs-specific navigation.
73
+ * Thin wrapper around `foldPath(stepIntoYjs, ...)`. Returns the
74
+ * `PathFoldResult` shape from core — `{ resolved, schema }`.
92
75
  *
93
- * When a `binding` is provided, each step computes the absolute schema
94
- * path and looks up the identity hash from `binding.forward`. This
95
- * identity hash is used instead of the field name at every product-field
96
- * boundary (root and nested).
76
+ * When a `binding` is provided, every product-field boundary uses the
77
+ * identity hash from `binding.forward` instead of the field name.
97
78
  *
98
- * Returns both the Yjs shared type (or plain value) and the schema at
99
- * the terminal position. For an empty path, returns the root map and
100
- * root schema.
101
- *
102
- * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
103
- * @param rootSchema - The root document schema
104
- * @param path - The path to resolve
105
- * @param binding - Optional SchemaBinding for identity-keyed navigation.
79
+ * For an empty path, returns the root map and root schema.
106
80
  */
107
81
  export function resolveYjsType(
108
82
  rootMap: Y.Map<any>,
109
83
  rootSchema: SchemaNode,
110
84
  path: Path,
111
85
  binding?: SchemaBinding,
112
- ): ResolvedYjs {
113
- let current: unknown = rootMap
114
- let schema = rootSchema
115
- // Track the accumulated absolute schema path for identity lookup.
116
- // Only string (key) segments contribute — index segments are structural
117
- // and don't participate in identity-keying.
118
- let absPath = ""
119
-
120
- for (let i = 0; i < path.length; i++) {
121
- const seg = path.segments[i]
122
- if (!seg) throw new Error(`Missing segment at index ${i}`)
123
- const nextSchema = advanceSchema(schema, seg)
124
-
125
- // Compute identity for this step if binding is provided and the
126
- // segment is a key (field name at a product boundary).
127
- let identity: string | undefined
128
- if (binding && seg.role === "key") {
129
- const segStr = seg.resolve() as string
130
- absPath = absPath ? `${absPath}.${segStr}` : segStr
131
- identity = binding.forward.get(absPath) as string | undefined
132
- }
133
-
134
- current = stepIntoYjs(current, seg, identity)
135
- schema = nextSchema
136
-
137
- // Sum variants are always PlainSchema — no CRDT containers inside.
138
- // Once we land on a sum, resolve remaining segments via plain JS
139
- // property access on the (JSON) value.
140
- if (schema[KIND] === "sum" && i + 1 < path.length) {
141
- for (let j = i + 1; j < path.length; j++) {
142
- const remaining = path.segments[j]
143
- if (!remaining) throw new Error(`Missing segment at index ${j}`)
144
- current = (current as Record<string, unknown>)?.[
145
- remaining.resolve() as string
146
- ]
147
- }
148
- return { resolved: current, schema }
149
- }
150
- }
151
-
152
- return { resolved: current, schema }
86
+ ): PathFoldResult {
87
+ return foldPath(rootMap, rootSchema, path, stepIntoYjs, binding)
153
88
  }