Skip to content

Replace snapshot-based undo/redo with Yorkie-native history in Docs#162

Merged
hackerwins merged 3 commits into
mainfrom
feat/yorkie-native-undo-redo
Apr 26, 2026
Merged

Replace snapshot-based undo/redo with Yorkie-native history in Docs#162
hackerwins merged 3 commits into
mainfrom
feat/yorkie-native-undo-redo

Conversation

@hackerwins

@hackerwins hackerwins commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Upgrade @yorkie-js/sdk and @yorkie-js/react from 0.7.6 to 0.7.7 (splitLevel>=2 undo/redo)
  • Remove snapshot-based undo/redo from YorkieDocStore — full document cloning on each undo caused duplicate paragraphs and conflicts with concurrent remote changes
  • Delegate undo/redo entirely to Yorkie's native doc.history API; snapshot() is now a no-op

Known Limitations

  • mergeBlock undo not yet supported by Yorkie Tree (editByPath merge reversal) — test skipped
  • Remaining updateBlock/updateTableCell calls (Phase 6-8 scope) bypass doc.update() boundaries, so those operations are not yet individually undoable

Test plan

  • pnpm verify:fast passes (622 tests)
  • Yorkie undo/redo tests: insertText, applyStyle, splitBlock, multiple undo/redo, canUndo/canRedo (7 pass, 1 skip)
  • Manual test: verify undo/redo in browser with live Yorkie server

🤖 Generated with Claude Code

Upgrade @yorkie-js/sdk and @yorkie-js/react from 0.7.6 to 0.7.7 which
ships splitLevel>=2 undo/redo support. Add `useYorkieUndo` option to
YorkieDocStore so callers can opt into Yorkie's native doc.history API
instead of the snapshot-based approach. When enabled, snapshot() is a
no-op and undo/redo/canUndo/canRedo delegate to doc.history. A
try/catch fallback disables the flag and falls back to snapshots if
Yorkie history throws.

Includes tests for insertText, applyStyle, splitBlock, multiple
undo/redo, and canUndo/canRedo via Yorkie history. mergeBlock undo is
skipped pending SDK support for editByPath merge reversal.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This update advances the Yorkie JS SDK from version 0.7.6 to 0.7.7, adds Yorkie-native undo/redo support to YorkieDocStore with fallback to snapshot-based history, and updates task documentation to reflect completed phases and remaining work for intent-preserving operations.

Changes

Cohort / File(s) Summary
Dependency Updates
packages/backend/package.json, packages/frontend/package.json
Bumps @yorkie-js/sdk and @yorkie-js/react from 0.7.6 to 0.7.7 across backend and frontend packages.
Yorkie-native Undo/Redo Implementation
packages/frontend/src/app/docs/yorkie-doc-store.ts
Adds constructor option useYorkieUndo to enable Yorkie-native history delegation. Refactors undo()/redo()/canUndo()/canRedo() to prefer doc.history.* methods when flag is enabled, with automatic fallback to snapshot-based behavior on history errors. snapshot() becomes a no-op when Yorkie history is active.
Undo/Redo Test Coverage
packages/frontend/tests/app/docs/yorkie-doc-store.test.ts
Adds comprehensive test suite for Yorkie-native undo/redo, covering text insertion, style application, block splitting, multi-step operations, and fallback semantics. Includes skipped test for unsupported mergeBlock undo behavior.
Task Progress Documentation
docs/tasks/active/20260403-intent-preserving-phase9-undo-redo-todo.md
Updates SDK version requirement, documents completed Tasks 1–2 (feature-flag integration and history-based undo/redo), refines Task 3 operation-type tests with skip annotations, and marks Task 4 call-site audit complete with remaining Phase 6–8 migration locations.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant YorkieDocStore as YorkieDocStore<br/>(useYorkieUndo: true)
    participant YorkieHistory as Yorkie<br/>doc.history
    participant SnapshotStack as Snapshot<br/>Stack (Fallback)

    App->>YorkieDocStore: undo()
    activate YorkieDocStore
    YorkieDocStore->>YorkieHistory: doc.history.undo()
    activate YorkieHistory
    YorkieHistory-->>YorkieDocStore: success / error
    deactivate YorkieHistory
    
    alt History call succeeds
        YorkieDocStore->>YorkieDocStore: Mark store dirty<br/>Clear cached doc
        YorkieDocStore-->>App: Undo applied
    else History call fails
        YorkieDocStore->>YorkieDocStore: Log warning<br/>Disable useYorkieUndo
        YorkieDocStore->>SnapshotStack: Fallback to snapshot undo
        activate SnapshotStack
        SnapshotStack-->>YorkieDocStore: Restore prior snapshot
        deactivate SnapshotStack
        YorkieDocStore-->>App: Fallback undo applied
    end
    deactivate YorkieDocStore
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A rabbit's ode to undo/redo grace:

The history flows through Yorkie's embrace,
With snapshots as safety when things go awry,
We split and we merge beneath the JS sky,
One flag switches backward with elegant trace,
And fallback's soft landing keeps errors at bay! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: replacing snapshot-based undo/redo with Yorkie-native history in the Docs component, which is the primary objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/yorkie-native-undo-redo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

When Yorkie history.undo() throws, return early instead of falling
through to snapshot-based path which has an empty stack. Add test
that simulates history failure, verifies graceful degradation, and
confirms snapshot-based undo works for subsequent operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 122.0s

Lane Status Duration
sheets:build ✅ pass 12.8s
docs:build ✅ pass 8.2s
verify:fast ✅ pass 60.6s
frontend:build ✅ pass 16.8s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 5.0s
cli:build ✅ pass 1.9s
verify:entropy ✅ pass 16.5s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/frontend/src/app/docs/yorkie-doc-store.ts (1)

1927-1948: ⚠️ Potential issue | 🔴 Critical

Fallback path crashes when Yorkie history throws — undoStack is always empty under useYorkieUndo.

When useYorkieUndo is enabled, snapshot() is a no-op (line 1919), so this.undoStack stays empty for the lifetime of the store. If doc.history.undo() throws, the catch block flips the flag and falls through to the snapshot path without a guard:

  • Line 1944: const previous = this.undoStack.pop()! → returns undefined (the ! only silences TS).
  • Line 1945: writeFullDocument(previous) → crashes on document.header / document.blocks access (line 2002, 2009).

The same bug is mirrored in redo() at line 1967.

Add a guard after the catch block to bail out when no snapshot history exists:

  catch (e) {
    console.warn('Yorkie undo failed, falling back to snapshot:', e);
    this.useYorkieUndo = false;
+   if (this.undoStack.length === 0) return;
  }

Apply the symmetrical guard in redo() for this.redoStack.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 1927 - 1948,
When Yorkie history throws inside undo() (and symmetrically in redo()), the code
flips useYorkieUndo to false and falls through to the snapshot-based path but
blindly pops from undoStack/redoStack which are empty because snapshot() was a
no-op; fix by adding a guard immediately after the catch that checks if
this.undoStack.length === 0 (for undo) / this.redoStack.length === 0 (for redo')
and simply return (no-op) if empty to avoid popping undefined and calling
writeFullDocument with an invalid document; update undo() and redo() around the
catch blocks where useYorkieUndo is set to false and reference the existing
methods/fields (undo(), redo(), snapshot(), undoStack, redoStack,
writeFullDocument, useYorkieUndo) when applying the guard.
🧹 Nitpick comments (3)
packages/frontend/tests/app/docs/yorkie-doc-store.test.ts (1)

1657-1667: Consider asserting the post-undo document state, not just canRedo.

After setDocument({ blocks: [block] }) and a single undo(), the document should revert to whatever pre-setDocument state Yorkie tracked (effectively empty). Asserting getDocument().blocks.length === 0 (or similar) here would catch regressions where undo() no-ops silently or only the redo flag flips without actually rewinding the tree.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/tests/app/docs/yorkie-doc-store.test.ts` around lines 1657
- 1667, The test currently only checks canRedo after calling yStore.undo();
update the test to also assert the document state actually reverted by calling
yStore.getDocument() and verifying blocks is empty (e.g.,
getDocument().blocks.length === 0 or deep-equal to the initial empty state)
after undo; locate the test using symbols makeBlock, yStore.setDocument,
yStore.undo, yStore.canRedo and add a post-undo assertion that confirms the tree
was rewound, not just the redo flag toggled.
packages/frontend/src/app/docs/yorkie-doc-store.ts (1)

1936-1939: One-way silent fallback loses all undo history — consider snapshotting on flip.

Once useYorkieUndo flips to false, the snapshot stacks are empty and stay that way until the next mutation triggers snapshot(). The user then has no way to undo anything that happened before the failed mergeBlock undo (which arguably is the action they most wanted to revert). Two mitigations worth considering:

  1. On flip, push a clone of the current document onto undoStack so at least the post-failure state is recoverable.
  2. Surface the flip via onRemoteChange?.() (or a new onUndoModeChange callback) so the host UI can disable the redo button / show a toast, since canUndo()/canRedo() semantics shift the moment the flag flips.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 1936 - 1939,
When catching the failed Yorkie undo in the catch block that sets useYorkieUndo
= false (around the mergeBlock undo path), preserve undo history and notify the
UI: push a deep clone of the current document state onto undoStack (or call
snapshot()) immediately before or when flipping useYorkieUndo so users can still
undo the last action, and call onRemoteChange?.() or emit a new
onUndoModeChange(...) callback right after flipping the flag so host UI can
update canUndo()/canRedo() and show a toast/disable redo; update references
around useYorkieUndo, undoStack, snapshot(), mergeBlock, and the
onRemoteChange/onUndoModeChange hooks accordingly.
docs/tasks/active/20260403-intent-preserving-phase9-undo-redo-todo.md (1)

45-47: Tie the Backspace-merge skip to the fallback bug in yorkie-doc-store.ts.

This skip is the same scenario that exposes the empty-snapshot-stack NPE in the undo() fallback path (see review on packages/frontend/src/app/docs/yorkie-doc-store.ts:1927-1948). Until that is fixed, hitting Backspace-merge-undo in the browser with useYorkieUndo: true won't just be "no-op"; it will throw. Worth adding a note here so whoever picks up the production rollout knows the prerequisite.

As per coding guidelines: docs/tasks/active/*-{todo,lessons}.md: For non-trivial tasks, use paired files in docs/tasks/active/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260403-intent-preserving-phase9-undo-redo-todo.md` around
lines 45 - 47, The Backspace-merge test was skipped because triggering the
undo() fallback path currently throws an empty-snapshot-stack NPE; fix the docs
by adding a note tying that skip to the bug in the undo() fallback and add a
paired lessons file as per the repo guideline. Update the todo entry to
explicitly state: Backspace-merge→undo is blocked until the empty-snapshot-stack
NPE in the undo() fallback (in yorkie-doc-store) is fixed; create the
corresponding docs/tasks/active/*-lessons.md file summarizing the root cause
(empty snapshot stack handling) and the required code change (guard against
empty snapshot stack or ensure snapshot push/pop invariants in the undo()
fallback). Ensure you reference the undo() fallback path and the
empty-snapshot-stack NPE in the new note so the production rollout owner knows
the prerequisite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 1927-1948: When Yorkie history throws inside undo() (and
symmetrically in redo()), the code flips useYorkieUndo to false and falls
through to the snapshot-based path but blindly pops from undoStack/redoStack
which are empty because snapshot() was a no-op; fix by adding a guard
immediately after the catch that checks if this.undoStack.length === 0 (for
undo) / this.redoStack.length === 0 (for redo') and simply return (no-op) if
empty to avoid popping undefined and calling writeFullDocument with an invalid
document; update undo() and redo() around the catch blocks where useYorkieUndo
is set to false and reference the existing methods/fields (undo(), redo(),
snapshot(), undoStack, redoStack, writeFullDocument, useYorkieUndo) when
applying the guard.

---

Nitpick comments:
In `@docs/tasks/active/20260403-intent-preserving-phase9-undo-redo-todo.md`:
- Around line 45-47: The Backspace-merge test was skipped because triggering the
undo() fallback path currently throws an empty-snapshot-stack NPE; fix the docs
by adding a note tying that skip to the bug in the undo() fallback and add a
paired lessons file as per the repo guideline. Update the todo entry to
explicitly state: Backspace-merge→undo is blocked until the empty-snapshot-stack
NPE in the undo() fallback (in yorkie-doc-store) is fixed; create the
corresponding docs/tasks/active/*-lessons.md file summarizing the root cause
(empty snapshot stack handling) and the required code change (guard against
empty snapshot stack or ensure snapshot push/pop invariants in the undo()
fallback). Ensure you reference the undo() fallback path and the
empty-snapshot-stack NPE in the new note so the production rollout owner knows
the prerequisite.

In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 1936-1939: When catching the failed Yorkie undo in the catch block
that sets useYorkieUndo = false (around the mergeBlock undo path), preserve undo
history and notify the UI: push a deep clone of the current document state onto
undoStack (or call snapshot()) immediately before or when flipping useYorkieUndo
so users can still undo the last action, and call onRemoteChange?.() or emit a
new onUndoModeChange(...) callback right after flipping the flag so host UI can
update canUndo()/canRedo() and show a toast/disable redo; update references
around useYorkieUndo, undoStack, snapshot(), mergeBlock, and the
onRemoteChange/onUndoModeChange hooks accordingly.

In `@packages/frontend/tests/app/docs/yorkie-doc-store.test.ts`:
- Around line 1657-1667: The test currently only checks canRedo after calling
yStore.undo(); update the test to also assert the document state actually
reverted by calling yStore.getDocument() and verifying blocks is empty (e.g.,
getDocument().blocks.length === 0 or deep-equal to the initial empty state)
after undo; locate the test using symbols makeBlock, yStore.setDocument,
yStore.undo, yStore.canRedo and add a post-undo assertion that confirms the tree
was rewound, not just the redo flag toggled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 096d1f48-c135-46cc-9310-781aba34ddb8

📥 Commits

Reviewing files that changed from the base of the PR and between 2683811 and f6c92e7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • docs/tasks/active/20260403-intent-preserving-phase9-undo-redo-todo.md
  • packages/backend/package.json
  • packages/frontend/package.json
  • packages/frontend/src/app/docs/yorkie-doc-store.ts
  • packages/frontend/tests/app/docs/yorkie-doc-store.test.ts

Comment thread packages/frontend/tests/app/docs/yorkie-doc-store.test.ts Outdated
Snapshot-based undo/redo writes the full document tree on each undo,
which can create duplicate paragraphs and conflicts with concurrent
remote changes. Replace entirely with Yorkie-native doc.history API.
Remove useYorkieUndo feature flag, undoStack/redoStack, and fallback
logic. snapshot() is now a permanent no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@hackerwins hackerwins changed the title Add Yorkie-native undo/redo with feature flag Replace snapshot-based undo/redo with Yorkie-native history in Docs Apr 26, 2026
@hackerwins
hackerwins merged commit f2bb764 into main Apr 26, 2026
1 check passed
@hackerwins
hackerwins deleted the feat/yorkie-native-undo-redo branch April 26, 2026 05:00
@codecov

codecov Bot commented Apr 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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