Skip to content

feat(plugin-sdk-value): sync through operational patches instead of whole-value diffs#2977

Open
ryanbonial wants to merge 4 commits into
portabletext:mainfrom
ryanbonial:sdk-value-patch-channel
Open

feat(plugin-sdk-value): sync through operational patches instead of whole-value diffs#2977
ryanbonial wants to merge 4 commits into
portabletext:mainfrom
ryanbonial:sdk-value-patch-channel

Conversation

@ryanbonial

@ryanbonial ryanbonial commented Jul 16, 2026

Copy link
Copy Markdown

Description

The plugin currently syncs by diffing whole-value snapshots in both directions, which makes concurrent edits to the same field last-writer-wins at best. @sanity/sdk 2.17.0 added an operational patch channel (remote-patches document events on the read side, editDocument(..., {preserveOperations: true}) on the write side, see sanity-io/sdk#1046), and this PR makes the plugin use it.

  • Incoming: SDKValuePlugin subscribes to remote-patches events, scopes document-rooted patches to the configured field, and applies them directly to the editor in every sync state. The editor merges them with in-flight local changes.
  • Outgoing: the editor's own mutation patches are converted to Sanity patch operations and dispatched with preserveOperations, so keyed operations reach the server intact.
  • Fallbacks: whole-value diff sync remains for sync events, patches that cannot be scoped or converted, and older SDK versions. Against @sanity/sdk-react < 2.17.0 the plugin behaves exactly as before.

ValueSyncPlugin gains optional onRemotePatches/pushPatches config for custom stores.

Repair sync after best-effort patch application

Applying remote patches is best-effort: keyed operations produced by another client against a different view of the document (for example both clients splitting the same span to format overlapping ranges) can fail to resolve and are skipped, leaving the editor diverged from the stored document. The second commit closes that gap:

  • Every remote patch application is now followed by a whole-value repair sync, immediately when no local edits are in flight or after the next mutation flush when they are. If the diff-based repair cannot converge, the plugin escalates to the full value sync machinery.
  • The repair diff no longer emits operations addressing items inside sidecar arrays (markDefs, marks), which the engine cannot resolve. Those are coalesced into whole-property set operations taken from the target value.

Testing

  • Unit tests for stringifyPatchPath, convertPatchesToSanity, scopeRemotePatches, and the new findSidecarArrayPath/toEngineSafePatches helpers.
  • Browser tests covering remote patches applying operationally, applying mid-typing without losing local changes, local edits pushing patches with the store converging purely through applyAll, the whole-value fallback when patch pushing throws, and repair convergence when remote patches cannot apply.
  • A new two-client browser rig (plugin.two-client.browser.test.tsx) connects two real editors through a mock server that applies patches best-effort and rebases each client's pending local changes, mirroring SDK optimistic behavior. It verifies convergence for concurrent typing in different blocks, concurrent same-range bolding, overlapping bold and link over the same range, and formatting conflicts during concurrent typing.
  • Verified end-to-end against a real dataset: two independent SanityInstance clients editing the same field through the Content Lake listener on @sanity/sdk-react 2.17.0, with concurrent typing and same-range formatting converging on both sides.

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e26dce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@portabletext/plugin-sdk-value Minor

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

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

@ryanbonial is attempting to deploy a commit to the Sanity Team on Vercel.

A member of the Team first needs to authorize it.

…server applier

Content Lake never creates array items when applying a set: a keyed or
indexed segment that does not resolve is a silent no-op (verified
against gradient's jsonpath matcher, which only auto-creates missing
object properties). The vendored applier mirrored an @sanity/sdk bug
that fabricated a degenerate item instead, which made the mock server
in the collision matrix more forgiving than the real backend.

The SDK side of this is fixed in sanity-io/sdk#1059, where the
fabricated items turned out to be the root cause of the
'Unexpected node type' crashes in the concurrency harness.
@ryanbonial

Copy link
Copy Markdown
Author

Update: root cause of the remaining harness crashes found, and it was in the SDK, not this plugin.

The Unexpected node type crashes in bold-overlap-range / link-overlap-range came from @sanity/sdk's local patch applier fabricating array items for unresolvable keyed set paths (e.g. children[_key=="X"].marks after a concurrent split removed span X). Content Lake silently no-ops those, so the server stayed valid while every receiving client's local snapshot grew phantom span-less {marks: []} children; this plugin's repair sync then faithfully converged the editor to the corrupted snapshot and the render layer threw. Fixed SDK-side in sanity-io/sdk#1059 with crash forensics and harness runs in the PR description (0 crashes across the full 13-scenario matrix after the fix, bold-overlap-range and link-overlap-range now score SAFE).

The commit just pushed here (6e26dce) is test-only: the vendored mock-server applier in the collision matrix mirrored the SDK's fabrication bug, making the mock more forgiving than the real backend. It now matches Content Lake (unresolvable keyed/indexed set segments are no-ops, verified against gradient), and the full collision matrix still passes against the corrected semantics.

For harness verification of this PR, pair it with an SDK build that includes sanity-io/sdk#1059.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant