@kyneta/yjs-schema 1.6.0 → 1.7.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 +17 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -202
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/__tests__/create.test.ts +1 -1
- package/src/__tests__/materialize.test.ts +227 -0
- package/src/__tests__/position.test.ts +7 -7
- package/src/__tests__/record-text-spike.test.ts +5 -5
- package/src/__tests__/structural-merge.test.ts +20 -20
- package/src/__tests__/substrate.test.ts +45 -0
- package/src/bind-yjs.ts +3 -5
- package/src/change-mapping.ts +62 -35
- package/src/index.ts +1 -2
- package/src/materialize.ts +109 -0
- package/src/populate.ts +23 -37
- package/src/substrate.ts +62 -52
- package/src/yjs-extract.ts +52 -0
- package/src/yjs-resolve.ts +30 -95
- package/src/__tests__/reader.test.ts +0 -685
- package/src/reader.ts +0 -174
package/src/substrate.ts
CHANGED
|
@@ -3,20 +3,30 @@
|
|
|
3
3
|
// Implements Substrate<YjsVersion> with:
|
|
4
4
|
// - Imperative local writes (prepare accumulates, onFlush applies in transact)
|
|
5
5
|
// - Persistent observeDeep event bridge for external changes
|
|
6
|
-
// -
|
|
6
|
+
// - Transaction-origin filter (`KYNETA_ORIGIN`) to ignore our own writes.
|
|
7
7
|
//
|
|
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
|
+
// `prepare` and `onFlush` accept `BatchOptions` and branch on
|
|
14
|
+
// `options?.replay`. The event bridge constructs the replay batch via
|
|
15
|
+
// `executeBatch(ctx, ops, { origin, replay: true })`; substrate-side
|
|
16
|
+
// work (transact, write) is skipped when `replay` is true because the
|
|
17
|
+
// native Y.Doc already absorbed the change. This makes `prepare` and
|
|
18
|
+
// `onFlush` total functions of their declared inputs — no hidden
|
|
19
|
+
// ambient state for the substrate-write decision. Context: jj:qpultxsw.
|
|
20
|
+
//
|
|
13
21
|
// Identity-keying: when a SchemaBinding is provided, all Y.Map key
|
|
14
22
|
// lookups and writes use the identity hash instead of the field name.
|
|
15
23
|
// The binding is threaded to the reader, event bridge, and write path.
|
|
16
24
|
|
|
17
25
|
import type {
|
|
26
|
+
BatchOptions,
|
|
18
27
|
ChangeBase,
|
|
19
28
|
Path,
|
|
29
|
+
PlainState,
|
|
20
30
|
PositionCapable,
|
|
21
31
|
ProductSchema,
|
|
22
32
|
Reader,
|
|
@@ -32,17 +42,19 @@ import type {
|
|
|
32
42
|
WritableContext,
|
|
33
43
|
} from "@kyneta/schema"
|
|
34
44
|
import {
|
|
45
|
+
applyChange,
|
|
35
46
|
BACKING_DOC,
|
|
36
47
|
buildWritableContext,
|
|
37
48
|
deriveSchemaBinding,
|
|
38
49
|
executeBatch,
|
|
39
50
|
KIND,
|
|
51
|
+
plainReader,
|
|
40
52
|
} from "@kyneta/schema"
|
|
41
53
|
import * as Y from "yjs"
|
|
42
54
|
import { applyChangeToYjs, eventsToOps } from "./change-mapping.js"
|
|
55
|
+
import { materializeYjsShadow } from "./materialize.js"
|
|
43
56
|
import { ensureContainers } from "./populate.js"
|
|
44
57
|
import { toYjsAssoc, YjsPosition } from "./position.js"
|
|
45
|
-
import { yjsReader } from "./reader.js"
|
|
46
58
|
import { YjsVersion } from "./version.js"
|
|
47
59
|
import { resolveYjsType } from "./yjs-resolve.js"
|
|
48
60
|
|
|
@@ -85,12 +97,6 @@ export function createYjsSubstrate(
|
|
|
85
97
|
// Accumulated changes from prepare(), drained by onFlush().
|
|
86
98
|
const pendingChanges: Array<{ path: Path; change: ChangeBase }> = []
|
|
87
99
|
|
|
88
|
-
// Re-entrancy guard: set true around our doc.transact() in onFlush
|
|
89
|
-
// AND around executeBatch in the event bridge. When true, prepare()
|
|
90
|
-
// skips Yjs-side work (changes are already applied by Yjs or about
|
|
91
|
-
// to be), and onFlush() skips transact/commit.
|
|
92
|
-
let inOurTransaction = false
|
|
93
|
-
|
|
94
100
|
// Stashed origin from merge for the event bridge to pick up.
|
|
95
101
|
let pendingMergeOrigin: string | undefined
|
|
96
102
|
|
|
@@ -108,8 +114,10 @@ export function createYjsSubstrate(
|
|
|
108
114
|
// The root Y.Map — all schema fields are children of this single map.
|
|
109
115
|
const rootMap = doc.getMap("root")
|
|
110
116
|
|
|
111
|
-
// The
|
|
112
|
-
|
|
117
|
+
// The shadow — a plain JS object materialized from the Y.Doc.
|
|
118
|
+
// Kept in sync by applyChange() in prepare().
|
|
119
|
+
const shadow: PlainState = materializeYjsShadow(doc, schema, binding)
|
|
120
|
+
const reader: Reader = plainReader(shadow)
|
|
113
121
|
|
|
114
122
|
// --- Substrate object ---
|
|
115
123
|
|
|
@@ -118,34 +126,44 @@ export function createYjsSubstrate(
|
|
|
118
126
|
|
|
119
127
|
reader: reader,
|
|
120
128
|
|
|
121
|
-
prepare(path: Path, change: ChangeBase): void {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
prepare(path: Path, change: ChangeBase, options?: BatchOptions): void {
|
|
130
|
+
// Local writes: apply eagerly to the shadow so reads are
|
|
131
|
+
// immediately consistent. Replay writes: skip — the shadow
|
|
132
|
+
// will be re-materialized from the Y.Doc in onFlush(replay).
|
|
133
|
+
if (!options?.replay) {
|
|
134
|
+
applyChange(shadow, path, change)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options?.replay) {
|
|
138
|
+
return
|
|
126
139
|
}
|
|
127
|
-
|
|
128
|
-
// wrappedPrepare (changefeed layer) still buffers the op.
|
|
140
|
+
pendingChanges.push({ path, change })
|
|
129
141
|
},
|
|
130
142
|
|
|
131
|
-
onFlush(
|
|
132
|
-
if (
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
doc.transact(() => {
|
|
138
|
-
for (const { path, change } of pendingChanges) {
|
|
139
|
-
applyChangeToYjs(rootMap, schema, path, change, binding)
|
|
140
|
-
}
|
|
141
|
-
}, KYNETA_ORIGIN)
|
|
142
|
-
pendingChanges.length = 0
|
|
143
|
-
} finally {
|
|
144
|
-
inOurTransaction = false
|
|
143
|
+
onFlush(options?: BatchOptions): void {
|
|
144
|
+
if (options?.replay) {
|
|
145
|
+
// Re-materialize shadow from the Y.Doc (already committed).
|
|
146
|
+
const fresh = materializeYjsShadow(doc, schema, binding)
|
|
147
|
+
for (const key of Object.keys(fresh)) {
|
|
148
|
+
shadow[key] = fresh[key]
|
|
145
149
|
}
|
|
150
|
+
for (const key of Object.keys(shadow)) {
|
|
151
|
+
if (!(key in fresh)) {
|
|
152
|
+
delete shadow[key]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return
|
|
146
156
|
}
|
|
147
|
-
|
|
148
|
-
//
|
|
157
|
+
if (pendingChanges.length === 0) return
|
|
158
|
+
// The KYNETA_ORIGIN tag lets the observeDeep bridge below
|
|
159
|
+
// recognise and skip the events we generate here, so the
|
|
160
|
+
// changefeed isn't fired twice for the same write.
|
|
161
|
+
doc.transact(() => {
|
|
162
|
+
for (const { path, change } of pendingChanges) {
|
|
163
|
+
applyChangeToYjs(rootMap, schema, path, change, binding)
|
|
164
|
+
}
|
|
165
|
+
}, KYNETA_ORIGIN)
|
|
166
|
+
pendingChanges.length = 0
|
|
149
167
|
},
|
|
150
168
|
|
|
151
169
|
context(): WritableContext {
|
|
@@ -232,7 +250,7 @@ export function createYjsSubstrate(
|
|
|
232
250
|
}
|
|
233
251
|
},
|
|
234
252
|
|
|
235
|
-
merge(payload: SubstratePayload,
|
|
253
|
+
merge(payload: SubstratePayload, options?: BatchOptions): void {
|
|
236
254
|
if (
|
|
237
255
|
payload.encoding !== "binary" ||
|
|
238
256
|
!(payload.data instanceof Uint8Array)
|
|
@@ -243,14 +261,14 @@ export function createYjsSubstrate(
|
|
|
243
261
|
)
|
|
244
262
|
}
|
|
245
263
|
// Stash origin for the event bridge to pick up
|
|
246
|
-
pendingMergeOrigin = origin
|
|
264
|
+
pendingMergeOrigin = options?.origin
|
|
247
265
|
try {
|
|
248
|
-
Y.applyUpdate(doc, payload.data, origin ?? "remote")
|
|
266
|
+
Y.applyUpdate(doc, payload.data, options?.origin ?? "remote")
|
|
249
267
|
} finally {
|
|
250
268
|
pendingMergeOrigin = undefined
|
|
251
269
|
}
|
|
252
270
|
// That's it — the observeDeep handler bridges events to the
|
|
253
|
-
// changefeed via executeBatch
|
|
271
|
+
// changefeed via executeBatch with `replay: true`.
|
|
254
272
|
},
|
|
255
273
|
}
|
|
256
274
|
|
|
@@ -283,15 +301,10 @@ export function createYjsSubstrate(
|
|
|
283
301
|
// Lazily ensure the context is built
|
|
284
302
|
const ctx = substrate.context()
|
|
285
303
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
executeBatch(ctx, ops, origin)
|
|
292
|
-
} finally {
|
|
293
|
-
inOurTransaction = false
|
|
294
|
-
}
|
|
304
|
+
// `replay: true` tells substrate.prepare/onFlush to skip native-side
|
|
305
|
+
// work (Yjs has already absorbed these ops via Y.applyUpdate) and
|
|
306
|
+
// surfaces on the Changeset for downstream filters (exchange echo).
|
|
307
|
+
executeBatch(ctx, ops, { origin, replay: true })
|
|
295
308
|
})
|
|
296
309
|
|
|
297
310
|
// For local mutations (KYNETA_ORIGIN): the observeDeep handler returns
|
|
@@ -417,7 +430,7 @@ export function createYjsReplica(doc: Y.Doc): Replica<YjsVersion> {
|
|
|
417
430
|
}
|
|
418
431
|
},
|
|
419
432
|
|
|
420
|
-
merge(payload: SubstratePayload,
|
|
433
|
+
merge(payload: SubstratePayload, _options?: BatchOptions): void {
|
|
421
434
|
if (
|
|
422
435
|
payload.encoding !== "binary" ||
|
|
423
436
|
!(payload.data instanceof Uint8Array)
|
|
@@ -477,17 +490,14 @@ export const yjsSubstrateFactory: SubstrateFactory<YjsVersion> = {
|
|
|
477
490
|
const doc = (replica as any)[BACKING_DOC] as Y.Doc
|
|
478
491
|
const binding = trivialBinding(schema)
|
|
479
492
|
// No identity injection for the standalone factory (no peerId).
|
|
480
|
-
|
|
481
|
-
// from hydrated state.
|
|
482
|
-
ensureContainers(doc, schema, true, binding)
|
|
493
|
+
ensureContainers(doc, schema, binding)
|
|
483
494
|
return createYjsSubstrate(doc, schema, binding)
|
|
484
495
|
},
|
|
485
496
|
|
|
486
497
|
create(schema: SchemaNode): Substrate<YjsVersion> {
|
|
487
|
-
// Fresh doc — unconditional ensureContainers (nothing to conflict with).
|
|
488
498
|
const doc = new Y.Doc()
|
|
489
499
|
const binding = trivialBinding(schema)
|
|
490
|
-
ensureContainers(doc, schema,
|
|
500
|
+
ensureContainers(doc, schema, binding)
|
|
491
501
|
return createYjsSubstrate(doc, schema, binding)
|
|
492
502
|
},
|
|
493
503
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// yjs-extract — shared value-extraction helpers for Yjs shared types.
|
|
2
|
+
//
|
|
3
|
+
// These functions are used by both the reader (yjsReader) and the
|
|
4
|
+
// materialize interpreter to convert Yjs shared types into plain values.
|
|
5
|
+
|
|
6
|
+
import type { RichTextDelta, RichTextSpan } from "@kyneta/schema"
|
|
7
|
+
import * as Y from "yjs"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract a plain value from a Yjs shared type or return a plain value as-is.
|
|
11
|
+
*
|
|
12
|
+
* - Y.Text → `.toJSON()` (string)
|
|
13
|
+
* - Y.Map → `.toJSON()` (plain object snapshot — for product/map reads)
|
|
14
|
+
* - Y.Array → `.toJSON()` (plain array snapshot)
|
|
15
|
+
* - Plain values (string, number, boolean, null) → returned as-is
|
|
16
|
+
*/
|
|
17
|
+
export function extractValue(resolved: unknown): unknown {
|
|
18
|
+
if (resolved instanceof Y.Text) {
|
|
19
|
+
return resolved.toJSON()
|
|
20
|
+
}
|
|
21
|
+
if (resolved instanceof Y.Map) {
|
|
22
|
+
return resolved.toJSON()
|
|
23
|
+
}
|
|
24
|
+
if (resolved instanceof Y.Array) {
|
|
25
|
+
return resolved.toJSON()
|
|
26
|
+
}
|
|
27
|
+
// Plain scalar value (string, number, boolean, null, etc.)
|
|
28
|
+
return resolved
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert a Y.Text's delta (Quill format) to a kyneta RichTextDelta.
|
|
33
|
+
*
|
|
34
|
+
* Yjs `.toDelta()` returns `{ insert: string, attributes?: Record<string, any> }[]`.
|
|
35
|
+
* Kyneta RichTextDelta is `{ text: string, marks?: MarkMap }[]`.
|
|
36
|
+
*/
|
|
37
|
+
export function yTextToRichTextDelta(ytext: Y.Text): RichTextDelta {
|
|
38
|
+
const delta = ytext.toDelta() as Array<{
|
|
39
|
+
insert: string
|
|
40
|
+
attributes?: Record<string, unknown>
|
|
41
|
+
}>
|
|
42
|
+
const spans: RichTextSpan[] = []
|
|
43
|
+
for (const d of delta) {
|
|
44
|
+
if (typeof d.insert !== "string") continue
|
|
45
|
+
const span: RichTextSpan =
|
|
46
|
+
d.attributes && Object.keys(d.attributes).length > 0
|
|
47
|
+
? { text: d.insert, marks: d.attributes }
|
|
48
|
+
: { text: d.insert }
|
|
49
|
+
spans.push(span)
|
|
50
|
+
}
|
|
51
|
+
return spans
|
|
52
|
+
}
|
package/src/yjs-resolve.ts
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
// yjs-resolve — Yjs-specific path resolution.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 —
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
49
|
-
current
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
*
|
|
91
|
-
*
|
|
73
|
+
* Thin wrapper around `foldPath(stepIntoYjs, ...)`. Returns the
|
|
74
|
+
* `PathFoldResult` shape from core — `{ resolved, schema }`.
|
|
92
75
|
*
|
|
93
|
-
* When a `binding` is provided,
|
|
94
|
-
*
|
|
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
|
-
*
|
|
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
|
-
):
|
|
113
|
-
|
|
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
|
}
|