fix: apply value-sync auto-resolutions to the block the engine receives#2968
Draft
christianhg wants to merge 1 commit into
Draft
fix: apply value-sync auto-resolutions to the block the engine receives#2968christianhg wants to merge 1 commit into
christianhg wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 9ea71e6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📦 Bundle Stats —
|
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 800.5 KB | +494 B, +0.1% |
| Internal (gzip) | 153.4 KB | +72 B, +0.0% |
| Bundled (raw) | 1.41 MB | +494 B, +0.0% |
| Bundled (gzip) | 317.6 KB | +75 B, +0.0% |
| Import time | 97ms | +0ms, +0.0% |
@portabletext/editor/behaviors
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 467 B | - |
| Internal (gzip) | 207 B | - |
| Bundled (raw) | 424 B | - |
| Bundled (gzip) | 171 B | - |
| Import time | 2ms | -0ms, -1.7% |
@portabletext/editor/plugins
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 2.7 KB | - |
| Internal (gzip) | 894 B | - |
| Bundled (raw) | 2.5 KB | - |
| Bundled (gzip) | 827 B | - |
| Import time | 7ms | +0ms, +2.0% |
@portabletext/editor/selectors
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 81.2 KB | - |
| Internal (gzip) | 14.9 KB | - |
| Bundled (raw) | 76.7 KB | - |
| Bundled (gzip) | 13.8 KB | - |
| Import time | 8ms | +0ms, +1.3% |
@portabletext/editor/traversal
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 28.1 KB | - |
| Internal (gzip) | 5.6 KB | - |
| Bundled (raw) | 27.9 KB | - |
| Bundled (gzip) | 5.5 KB | - |
| Import time | 6ms | +0ms, +0.6% |
@portabletext/editor/utils
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 29.7 KB | - |
| Internal (gzip) | 6.2 KB | - |
| Bundled (raw) | 27.1 KB | - |
| Bundled (gzip) | 5.8 KB | - |
| Import time | 6ms | +0ms, +0.6% |
🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — @portabletext/markdown
Compared against main (84d84b2d)
| Metric | Value | vs main (84d84b2) |
|---|---|---|
| Internal (raw) | 53.8 KB | - |
| Internal (gzip) | 9.8 KB | - |
| Bundled (raw) | 348.9 KB | - |
| Bundled (gzip) | 96.3 KB | - |
| Import time | 38ms | +1ms, +3.2% |
🗺️ View treemap · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
`validateValue` runs at sync ingress and can auto-resolve certain invalid shapes (a child missing `_key`, empty `children`, unused `markDefs`). The resolution existed only as patches: the changed-block path emitted them outbound (mid-session, non-readOnly) while the raw, un-repaired block proceeded into `updateBlock`/`replaceBlock`, and the insert path dropped them entirely. The engine ended up holding the un-repaired shape, a keyless child among them, which the engine's addressing model cannot represent, and diverged from the document: the emitted patch minted one key while engine normalization minted another for the same node (observable in the old test pins, which expected the second mint). Apply the resolution to the block before it enters the engine, at both call sites, via `applyAll`. The emitted patches are unchanged and now agree with engine state: one key, minted once, on both sides. The engine never holds the un-repaired shape. Emission semantics are untouched: the insert path still emits nothing, and the changed-block path still emits only mid-session for non-readOnly editors. Test pins shift accordingly: the self-solving and container-normalization suites now expect the first mint's key, and the operation-stream scenario for empty-children blocks expects the placeholder span inside the block's own insert operation rather than as a separate normalization fix. The normalization-adjacency contract is re-pinned on duplicate child keys, which have no ingress check and are still repaired by normalization.
christianhg
force-pushed
the
fix-sync-autoresolve-repair
branch
from
July 10, 2026 07:02
fa779b6 to
9ea71e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Probing the value sync's repair paths (follow-up territory from #2965) surfaced this: send an editor a mid-session
update valuewhose changed block has a child missing its_key, and the editor ends up holding that keyless child, a shape its own addressing model can't represent (React immediately warns about duplicate keys rendering it), while the document receives a repair patch the editor never applied to itself.The mechanism:
validateValueruns at sync ingress and can auto-resolve certain defects (child missing_key, emptychildren, unusedmarkDefs). But the resolution existed only as patches. The changed-block path emitted them outbound (gated to mid-session, non-readOnly, with aconsole.warn) while passing the raw block intoupdateBlock/replaceBlock; the insert path computed them and dropped them entirely. So the repair forked: the document got the resolution's minted key, the engine got the un-repaired block, and engine normalization later minted a different key for the same node. The old test pins are the fossil record of this double mint,pteWarningsSelfSolvingexpectedk3, the key normalization minted aftervalidateValue'sk2was discarded.The fix applies the resolution to the block before it enters the engine, at both call sites, via
applyAllfrom@portabletext/patches(the same constructorsvalidateValuebuilds its resolutions with). The emitted patches are byte-identical to before and now agree with engine state: one key, minted once, on both sides. Emission semantics are deliberately untouched, the insert path still emits nothing, the changed path still emits only mid-session for non-readOnly editors; changing when repairs are emitted is the pending-events design work, tracked separately.Two regression scenarios in
event.update-value.test.tsxpin the contract: a mid-session keyless-child update repairs once (emitted patch key === engine key, exact values) and the sync survives to apply a subsequent update; a startup value with a keyless child is repaired in the engine while emitting nothing (no mutation leaves a pristine editor on open). The survival assertion needs a beat over a second: the sync machine parks inbusywhile its own emitted mutation flushes and re-polls on a 1s timer, which is also worth naming because an earlier probe mistook that delay for a permanent wedge; there is no wedge, the machine recovers by design.Semantic deltas, all deliberate and pinned: repaired blocks now carry the first mint's key (
pteWarningsSelfSolving,container-normalizationpins shift from the second mint to the first), and an empty-children block arrives with its placeholder span inside the block's owninsertoperation instead of as a separate normalization fix op (event.operation). The normalization-adjacency contract that scenario used to exercise is re-pinned on duplicate child keys, which have no ingress check and are still repaired by normalization, so the contract keeps a living fixture. Full unit (852) and browser (1718) suites pass.