Skip to content

Conversation

@JOOHOJANG
Copy link
Contributor

@JOOHOJANG JOOHOJANG commented Dec 1, 2025

What this PR does / why we need it?

Implement reverse of EditOperation based on ideas of #1090.
This PR only covers undo/redo of Text's edit operation, style is not implemented yet.

It consists of the following three major components:

  • Store reverse operations when executing local edits
  • Reconcile the undo stack when receiving remote changes
  • Clean up the undo/redo stacks when receiving a snapshot

Any background context you want to provide?

Please read #1090 to understand basis of our undo/redo architecture.

What are the relevant tickets?

Fixes #

Checklist

  • Added relevant tests or not required
  • Addressed and resolved all CodeRabbit review comments
  • Didn't break anything

Summary by CodeRabbit

  • New Features

    • Improved undo/redo handling across clients and editor integration; added undo-aware edit support and cursor-preserving snapshot sync.
  • Bug Fixes

    • Cleared stale undo/redo state after local changes and reconciled history with applied edits for correct undo/redo behavior.
    • Better handling of remote edits during synchronization, preserving selection and convergence.
  • Tests

    • Added comprehensive integration tests covering text history, undo/redo, and multi-client convergence.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Walkthrough

Adds multi-user undo/redo: CRDT edits now return removed values; new position normalization/refinement APIs; EditOperation becomes undo-aware with reverse-op generation and reconciliation; History gains reconciliation/clear methods; Document integrates history reconciliation/clearing; CodeMirror example and tests updated.

Changes

Cohort / File(s) Summary
Example: CodeMirror
examples/vanilla-codemirror6/package.json, examples/vanilla-codemirror6/src/main.ts
Add @codemirror/view dependency; add CodeMirror undo/redo keymap and wiring; adjust snapshot/sync flow and remote-change handling; preserve/restore cursor during snapshot sync.
Change API
packages/sdk/src/document/change/change.ts
Change.execute() now returns operations: Array<Operation> in addition to opInfos and reverseOps.
CRDT: RGATreeSplit
packages/sdk/src/document/crdt/rga_tree_split.ts
edit() return extended to include removed node values; add normalizePos() and refinePos() APIs for position mapping/refinement; call sites updated to receive removed values.
CRDT: Text
packages/sdk/src/document/crdt/text.ts
edit() return extended to include removed values; added refinePos(), normalizePos(), and posToIndex() delegations; valueChange entries enriched with content/attributes.
Edit Operation
packages/sdk/src/document/operation/edit_operation.ts
EditOperation gains optional isUndoOp, builds reverse operations from removed values, and exposes normalizePos() and reconcileOperation() to adjust ranges for undo handling.
History
packages/sdk/src/document/history.ts
Add clearUndo() to flush undo stack and reconcileTextEdit() to update undo/redo entries based on applied text edits.
Document
packages/sdk/src/document/document.ts
Use change.execute() operations list to reconcile history for text edits; clear history in key flows; add private clearHistory helper.
Tests
packages/sdk/test/integration/history_text_test.ts
New integration tests covering insert/delete/replace undo-redo, chained operations, multi-client convergence, and overlapping/containing edit scenarios (some style tests skipped).

Sequence Diagram(s)

sequenceDiagram
    participant Editor as Client/Editor
    participant Doc as Document
    participant CRDT as CRDT (CRDTText / RGATreeSplit)
    participant Hist as History
    participant Remote as Remote/Network

    Editor->>Doc: apply local Change (EditOperation)
    Doc->>CRDT: CRDTText.edit(...) → changes + removedValues
    CRDT-->>Doc: changes, removedValues
    Doc->>Doc: Change.execute() → operations[], opInfos, reverseOps
    Doc->>Hist: push operation(s) to undo stack
    Doc->>Remote: publish operations
    Remote-->>Doc: receive remote EditOperation
    Doc->>Hist: reconcileTextEdit(parentCreatedAt, rangeFrom, rangeTo, contentLen)
    Hist-->>Doc: undo/redo entries reconciled
    Doc->>CRDT: apply remote edit (normalize/refine positions if undo)
    CRDT-->>Doc: applied changes
    Doc->>Editor: notify sync (syncText / handleOperations)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect EditOperation: isUndoOp propagation, toReverseOperation, normalizePos, reconcileOperation.
  • Verify RGATreeSplit.normalizePos/refinePos and removedValues collection correctness.
  • Validate History.reconcileTextEdit() and clearUndo() interactions with Document flows.
  • Confirm Change.execute() return shape is handled across callers.
  • Review new integration tests for coverage and potential flakes.

Possibly related issues

Poem

🐰
I nibbled hops where indices play,
I stitched lost bytes back into sway.
With refined hops and reversed delight,
Undo and redo waltz through the night.
Hop, sync, converge — a rabbit's bright bite! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Text undo/redo" directly and clearly describes the main change: implementing undo/redo functionality for Text edit operations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch text-reverse

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd7e20 and dda181a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • packages/sdk/src/document/crdt/rga_tree_split.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/sdk/src/document/crdt/rga_tree_split.ts (3)
packages/sdk/src/document/crdt/gc.ts (1)
  • GCPair (24-27)
packages/sdk/src/util/resource.ts (1)
  • DataSize (27-37)
packages/sdk/src/util/error.ts (1)
  • YorkieError (85-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (22.x)
🔇 Additional comments (2)
packages/sdk/src/document/crdt/rga_tree_split.ts (2)

600-613: LGTM! Return type correctly expanded.

The expanded return type to include Array<T> as the fifth tuple element is consistent with the new functionality of collecting and returning removed values during edit operations.


798-822: LGTM! Correct implementation of position normalization.

The normalizePos method correctly accumulates lengths by walking backward through the chain using getLength(), which properly treats removed nodes as length 0. The error handling and final position calculation are appropriate.


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.

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 80.37383% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.97%. Comparing base (560f07d) to head (dda181a).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...kages/sdk/src/document/operation/edit_operation.ts 77.35% 5 Missing and 7 partials ⚠️
packages/sdk/src/document/crdt/rga_tree_split.ts 77.77% 3 Missing and 3 partials ⚠️
packages/sdk/src/document/crdt/text.ts 66.66% 2 Missing ⚠️
packages/sdk/src/document/document.ts 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1126      +/-   ##
==========================================
+ Coverage   80.92%   80.97%   +0.05%     
==========================================
  Files          70       72       +2     
  Lines        5682     5899     +217     
  Branches     1172     1233      +61     
==========================================
+ Hits         4598     4777     +179     
- Misses        719      738      +19     
- Partials      365      384      +19     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JOOHOJANG
Copy link
Contributor Author

If the review progresses and there is further development, I plan to add a design guideline later as well.

@JOOHOJANG JOOHOJANG marked this pull request as ready for review December 3, 2025 16:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (8)
packages/sdk/test/integration/history_text_test.ts (4)

9-50: Redundant length calculation pattern.

The fallback t.length ?? t.toString().length appears in every case. Since Text.length should always be defined (per the CRDT implementation), the fallback seems unnecessary and adds noise.

Consider simplifying to just t.length:

 case 'insert': {
-  const len = t.length ?? t.toString().length;
+  const len = t.length;
   t.edit(len, len, 'X');
   break;
 }

Apply similar changes to other cases if t.length is reliably defined.


52-74: Missing style case handler in applyTextOp2.

Unlike applyTextOp1, this function lacks a style case. If style is passed, the switch will silently do nothing. Consider adding a case or a default to throw for unhandled operations to avoid silent failures when tests are extended.

       case 'replace': {
         const len = t.length ?? t.toString().length;
         if (len > 0) t.edit(0, 1, 'Z');
         else t.edit(0, 0, 'Z');
         break;
       }
+      default:
+        throw new Error(`Unhandled operation: ${op}`);
     }

142-142: Non-English comment should be translated.

The comment // 텍스트 스냅샷 저장 (Korean for "save text snapshot") should be in English for consistency with the rest of the codebase.

-          // 텍스트 스냅샷 저장
+          // Save text snapshots
           const S: Array<string> = [];

176-216: Multi-client convergence test looks good but consider adding redo verification.

The test validates document convergence after operations and undos, but doesn't verify that redo also converges correctly. Since undo/redo is the feature being tested, adding a redo step would provide more complete coverage.

Consider adding redo verification:

           assert.equal(
             d1.toSortedJSON(),
             d2.toSortedJSON(),
             'Mismatch after both undos',
           );
+
+          d1.history.redo();
+          d2.history.redo();
+
+          await c1.sync();
+          await c2.sync();
+          await c1.sync();
+
+          assert.equal(
+            d1.toSortedJSON(),
+            d2.toSortedJSON(),
+            'Mismatch after both redos',
+          );
         }, task.name);
examples/vanilla-codemirror6/src/main.ts (1)

51-58: Whitespace on line 51 and subscription condition logic.

The condition event.type === 'remote-change' || event.source === 'undoredo' correctly handles both remote changes and undo/redo events. However, line 51 has trailing whitespace that should be cleaned up.

-
+ 
   doc.subscribe('$.content', (event) => {
packages/sdk/src/document/crdt/text.ts (1)

398-410: New position utility methods are clean delegations.

Both refinePos and normalizePos properly delegate to the underlying rgaTreeSplit implementation. The JSDoc comments are concise but could benefit from brief descriptions of what "refine" vs "normalize" mean semantically.

Consider enhancing documentation:

   /**
-   * `refinePos` refines the given RGATreeSplitPos.
+   * `refinePos` refines the given position by adjusting it to account for
+   * nodes that may have been split or removed since the position was created.
    */
   public refinePos(pos: RGATreeSplitPos): RGATreeSplitPos {
     return this.rgaTreeSplit.refinePos(pos);
   }
packages/sdk/src/document/operation/edit_operation.ts (2)

177-202: normalizePos method duplicates parent object lookup pattern.

The lookup and validation logic (lines 181-195) is nearly identical to the pattern in execute. Consider extracting a private helper method to reduce duplication.

+  private getTextFromRoot<A extends Indexable>(root: CRDTRoot): CRDTText<A> {
+    const parentObject = root.findByCreatedAt(this.getParentCreatedAt());
+    if (!parentObject) {
+      throw new YorkieError(
+        Code.ErrInvalidArgument,
+        `fail to find ${this.getParentCreatedAt()}`,
+      );
+    }
+    if (!(parentObject instanceof CRDTText)) {
+      throw new YorkieError(
+        Code.ErrInvalidArgument,
+        `only Text operations are supported`,
+      );
+    }
+    return parentObject as CRDTText<A>;
+  }

198-201: normalizePos returns relative offsets, not absolute indices.

The method returns [rangeFrom, rangeTo] where both values are getRelativeOffset() from the normalized positions. The method name suggests it normalizes positions, but it actually returns relative offsets. This could be confusing for callers. Consider renaming to getNormalizedOffsets or updating the JSDoc to clarify.

   /**
-   * `normalizePos` normalizes the position of the edit operation.
+   * `normalizePos` returns the relative offsets of the normalized from/to positions.
+   * @returns A tuple [fromOffset, toOffset] representing the relative offsets.
    */
   public normalizePos<A extends Indexable>(root: CRDTRoot): [number, number] {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d5fb05 and 35648d0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • examples/vanilla-codemirror6/package.json (1 hunks)
  • examples/vanilla-codemirror6/src/main.ts (5 hunks)
  • packages/sdk/src/document/change/change.ts (3 hunks)
  • packages/sdk/src/document/crdt/rga_tree_split.ts (3 hunks)
  • packages/sdk/src/document/crdt/text.ts (5 hunks)
  • packages/sdk/src/document/document.ts (4 hunks)
  • packages/sdk/src/document/history.ts (3 hunks)
  • packages/sdk/src/document/operation/edit_operation.ts (6 hunks)
  • packages/sdk/test/integration/history_text_test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-01T04:32:18.880Z
Learnt from: emplam27
Repo: yorkie-team/yorkie-js-sdk PR: 1037
File: packages/sdk/src/document/json/array.ts:574-598
Timestamp: 2025-08-01T04:32:18.880Z
Learning: ArrayProxy.insertAfterInternal method returns CRDT elements, not target CRDT arrays, in packages/sdk/src/document/json/array.ts.

Applied to files:

  • packages/sdk/src/document/crdt/rga_tree_split.ts
  • packages/sdk/src/document/crdt/text.ts
📚 Learning: 2025-03-17T07:16:43.740Z
Learnt from: hackerwins
Repo: yorkie-team/yorkie-js-sdk PR: 954
File: examples/react-flow/src/main.tsx:44-59
Timestamp: 2025-03-17T07:16:43.740Z
Learning: In the react-flow example for yorkie-js-sdk, using a date-based document key (`react-flow-YYYYMMDD`) is an intentional design pattern to create a new document each day.

Applied to files:

  • examples/vanilla-codemirror6/src/main.ts
🧬 Code graph analysis (6)
packages/sdk/test/integration/history_text_test.ts (2)
packages/sdk/src/document/document.ts (1)
  • Document (512-1970)
packages/sdk/test/integration/integration_helper.ts (1)
  • withTwoClientsAndDocuments (37-69)
packages/sdk/src/document/history.ts (1)
packages/sdk/src/document/operation/edit_operation.ts (1)
  • EditOperation (35-308)
packages/sdk/src/document/crdt/rga_tree_split.ts (3)
packages/sdk/src/document/crdt/gc.ts (1)
  • GCPair (24-27)
packages/sdk/src/util/resource.ts (1)
  • DataSize (27-37)
packages/sdk/src/util/error.ts (1)
  • YorkieError (85-96)
examples/vanilla-codemirror6/src/main.ts (2)
examples/profile-stack/main.js (1)
  • doc (9-11)
examples/vanilla-codemirror6/src/type.ts (2)
  • YorkieDoc (3-5)
  • YorkiePresence (7-10)
packages/sdk/src/document/operation/edit_operation.ts (8)
packages/sdk/src/yorkie.ts (2)
  • TimeTicket (70-70)
  • Indexable (53-53)
packages/sdk/src/document/time/ticket.ts (1)
  • TimeTicket (51-186)
packages/sdk/public/devtool/text.js (1)
  • fromPos (666-666)
packages/sdk/src/document/crdt/rga_tree_split.ts (1)
  • RGATreeSplitPos (167-246)
packages/sdk/src/document/crdt/text.ts (2)
  • CRDTTextValue (73-207)
  • CRDTText (213-554)
packages/sdk/src/document/document.ts (1)
  • Indexable (392-392)
packages/sdk/src/document/crdt/root.ts (1)
  • CRDTRoot (78-411)
packages/sdk/src/util/error.ts (1)
  • YorkieError (85-96)
packages/sdk/src/document/crdt/text.ts (4)
packages/sdk/src/document/crdt/gc.ts (1)
  • GCPair (24-27)
packages/sdk/src/api/yorkie/v1/resources_pb.ts (1)
  • DataSize (3151-3189)
packages/sdk/src/util/resource.ts (1)
  • DataSize (27-37)
packages/sdk/src/document/crdt/rga_tree_split.ts (2)
  • RGATreeSplitPosRange (248-248)
  • RGATreeSplitPos (167-246)
🔇 Additional comments (24)
examples/vanilla-codemirror6/package.json (1)

16-16: LGTM!

The addition of @codemirror/view dependency is appropriate for enabling CodeMirror 6 view-layer features needed for undo/redo keybindings.

packages/sdk/src/document/crdt/rga_tree_split.ts (2)

607-674: LGTM!

The extended return type now includes removedValues as a fifth element, correctly collected from removed nodes. This enables the undo/redo feature to restore previously removed text content.


798-806: LGTM!

The normalizePos method correctly converts a local position to an absolute offset measured from the head, which is essential for position reconciliation during undo/redo operations.

packages/sdk/src/document/history.ts (2)

102-107: LGTM!

The clearUndo method follows the same pattern as clearRedo and correctly resets the undo stack.


161-186: LGTM!

The reconcileTextEdit method correctly scans both undo and redo stacks, identifies matching EditOperation instances by parentCreatedAt, and reconciles their positions. This ensures undo/redo operations remain consistent when concurrent text edits occur.

packages/sdk/src/document/document.ts (3)

1332-1333: LGTM!

Clearing history after applying the snapshot and re-applying local changes is correct. The old undo/redo operations would reference stale positions and nodes that may no longer exist in the new document state.


1349-1352: LGTM!

The clearHistory helper method cleanly encapsulates clearing both undo and redo stacks.


1447-1463: LGTM!

The integration correctly reconciles undo/redo history when remote text edits are applied. By iterating over actually executed operations and reconciling positions for EditOperation instances, this ensures that pending undo/redo operations remain valid after concurrent edits from other clients.

packages/sdk/src/document/change/change.ts (1)

153-196: LGTM!

The enhanced execute method now returns the list of successfully executed operations alongside opInfos and reverseOps. This enables the document layer to reconcile text edit positions based on what was actually applied, which is essential for maintaining undo/redo consistency across concurrent edits.

packages/sdk/test/integration/history_text_test.ts (5)

1-7: LGTM: Clean imports and clear type definition.

The TextOp type and ops array are well-defined. The TODO comment appropriately tracks pending work for style operations and multi-client tests.


125-169: Well-structured chained operations test with comprehensive coverage.

The triple-nested loop effectively covers all 27 combinations of insert, delete, replace operations. The snapshot-based verification approach is sound for validating step-by-step undo correctness.


218-261: Overlapping edits test provides good edge case coverage.

The test correctly sets up overlapping edit ranges [2,6) and [4,9) and validates convergence after both operations and undos. This is a critical scenario for CRDT undo/redo.


263-308: Containing edits test covers important nested edit scenario.

The outer [2,9) and inner [4,6) edit ranges test the containment case well. The undo ordering (d2 then d1) is intentionally different from the edit ordering, which tests robustness.


310-365: Style test is appropriately skipped pending implementation.

The skipped test provides a clear template for future style undo/redo testing, which aligns with the PR scope (edit only, not style).

examples/vanilla-codemirror6/src/main.ts (2)

146-170: Well-structured syncText function with proper cursor restoration.

The function correctly handles full text replacement with cursor position preservation via the presence selection. The snapshot subscription ensures re-sync when document state is replaced.


63-70: Correct removal of undo/redo from tracked user events.

Since undo/redo is now handled via custom keybindings that directly interact with doc.history, removing them from the tracked events array prevents duplicate handling.

packages/sdk/src/document/crdt/text.ts (3)

243-249: Extended edit return type properly documented.

The 5-tuple return type [Array<TextChange<A>>, Array<GCPair>, DataSize, RGATreeSplitPosRange, Array<CRDTTextValue>] correctly adds removed values as the final element. This maintains backward compatibility since existing code can ignore the new element.


257-274: Edit method correctly propagates removed values from RGATreeSplit.

The destructuring captures removedValues from the underlying edit call and passes it through in the return tuple. This enables undo operations to reconstruct deleted content.


527-535: New posToIndex method with preferToLeft parameter.

The method correctly delegates to rgaTreeSplit.posToIndex with the optional preferToLeft parameter. The underlying RGATreeSplit implementation accepts both pos and preferToLeft parameters and uses them appropriately for cursor positioning during undo/redo operations.

packages/sdk/src/document/operation/edit_operation.ts (5)

40-56: Optional isUndoOp flag properly integrated into constructor.

The new isUndoOp field is correctly added as an optional parameter to maintain backward compatibility. The field is assigned in the constructor body.


105-109: Position refinement for undo operations is correctly placed.

Refining positions before the edit ensures that the undo operation targets the correct location even if the text structure has changed since the original operation. The refinement only occurs for undo operations, avoiding overhead for normal edits.


207-261: reconcileOperation conditions serve valid purposes and are not redundant.

Analysis of the flagged conditions reveals they are purposeful, not redundant:

  1. Line 241 rangeFrom !== rangeTo: This excludes zero-length ranges from the "fully overlap: contains" case. An empty range cannot meaningfully contain another range, so the guard is necessary.

  2. Line 245 a !== b: This ensures the operation spans a non-zero range before applying the reconciliation logic. A single-position operation (where a === b) should not be reconciled using this branch.

The boundary equality cases are intentionally handled by strict inequality checks (<, > vs <=, >=), which systematically partition overlap scenarios without double-handling edge cases. However, adding unit tests for boundary conditions would improve confidence in the logic, as no existing tests were found for reconcileOperation.


166-169: Remove this review concern — the code handles all cases correctly.

The content property is strictly typed as string (never undefined), so the optional chaining and nullish coalescing are unnecessary but harmless. For pure deletions where content is an empty string '', the calculation correctly yields normalizedFromPos.offset + 0, which is the correct range for the reverse operation. The reverse operation logic is sound: it deletes what was inserted (this.content.length) and restores what was deleted (restoredContent). Extensive test coverage with pure deletion cases (40+ instances across test files) confirms this works as intended.


226-229: No action needed — mutation of fromPos and toPos is intentional.

The reconcile method is part of the abstract Operation interface contract and is specifically designed to update operation state based on transformation deltas. Creating new RGATreeSplitPos instances and assigning them to this.fromPos and this.toPos is the intended behavior, not a side effect. There is no evidence in the codebase of operations being reused after reconciliation or of this pattern causing issues.

Comment on lines +828 to +853
public refinePos(pos: RGATreeSplitPos): RGATreeSplitPos {
let node = this.findFloorNode(pos.getID());
if (!node) {
throw new YorkieError(
Code.ErrInvalidArgument,
`the node of the given id should be found: ${pos.getID().toTestString()}`,
);
}

let offsetInPart = pos.getRelativeOffset();
let partLen = node.getContentLength();

while (offsetInPart > partLen) {
offsetInPart -= partLen;
const next: RGATreeSplitNode<T> | undefined = node!.getNext();

if (!next) {
return RGATreeSplitPos.of(node.getID(), partLen);
}

node = next;
partLen = node.getLength();
}

return RGATreeSplitPos.of(node.getID(), offsetInPart);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for tests that verify refinePos behavior
rg -n "refinePos" --type=ts -A 5 -B 2

Repository: yorkie-team/yorkie-js-sdk

Length of output: 3764


🏁 Script executed:

# Search for getContentLength and getLength definitions
rg -n "getContentLength\(\)|getLength\(\)" packages/sdk/src/document/crdt/rga_tree_split.ts --type=ts -B 2 -A 3

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1498


🏁 Script executed:

# Examine the RGATreeSplitNode class to understand these methods
ast-grep --pattern 'class RGATreeSplitNode {
  $$$
}'

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


🏁 Script executed:

# Search for findFloorNode to understand if it can return removed nodes
rg -n "findFloorNode" packages/sdk/src/document/crdt/rga_tree_split.ts --type=ts -A 10 -B 2

Repository: yorkie-team/yorkie-js-sdk

Length of output: 2734


🏁 Script executed:

# Search for test cases that test refinePos with removed content
rg -n "refinePos" --type=ts -l | xargs grep -l "remov\|delete" | head -5

Repository: yorkie-team/yorkie-js-sdk

Length of output: 210


🏁 Script executed:

# Search specifically in test files for refinePos tests
fd ".*\.test\.ts$|.*\.spec\.ts$" --type=f | xargs rg -l "refinePos" 2>/dev/null | head -3

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


Fix inconsistent length calculation for initial node in refinePos.

Line 838 uses getContentLength() (ignores removal status) for the initial node, but line 849 uses getLength() (returns 0 for removed nodes) for subsequent nodes. This violates the documented behavior (line 812-813) that states: "Counts only live characters: removed nodes are treated as length 0."

When the initial node is removed, the current code incorrectly uses its full content length instead of treating it as 0. Change line 838 to use node.getLength() for consistency with the loop and the documented specification.

🤖 Prompt for AI Agents
In packages/sdk/src/document/crdt/rga_tree_split.ts around lines 828 to 853, the
initial node length is obtained with node.getContentLength() (line ~838) which
ignores removal status, causing removed nodes to be counted; change that call to
node.getLength() so the initial node is treated the same as subsequent nodes in
the loop, and keep the rest of the logic—assign partLen = node.getLength() and
let the while loop and return statements operate as before.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
examples/vanilla-codemirror6/src/main.ts (1)

117-145: Remove debug logging and confirm keymap precedence over CodeMirror history

The custom cmUndoRedoKeymap correctly binds Mod-z / Mod-Shift-z to doc.history.undo/redo, but the inline console.log('undo') / console.log('redo') are debug artifacts and should be removed before merge. Also, since basicSetup includes CodeMirror’s own history/keymap, it’s worth confirming via manual testing that your bindings reliably win for these keys (no double-undo or missed calls).

Apply this minimal diff to drop the logs:

   const cmUndoRedoKeymap = keymap.of([
     {
       key: 'Mod-z',
       run: () => {
-        // To check undo works properly
-        console.log('undo');
         if (doc.history.canUndo()) {
           doc.history.undo();
-          
         }
         return true;
       },
     },
     {
       key: 'Mod-Shift-z',
       run: () => {
-        // To check redo works properly
-        console.log('redo');
         if (doc.history.canRedo()) {
           doc.history.redo();
-         
         }
         return true;
       },
     },
   ]);
🧹 Nitpick comments (1)
examples/vanilla-codemirror6/src/main.ts (1)

23-27: Dynamic fallback docKey avoids accidental document sharing

Using a unique fallback key (vanilla-codemirror6-${Date.now()}) means users without a dockey query parameter no longer collide into the same document, while explicit sharing still works via the URL param. This matches the intent of avoiding unintended shared docs; choose this per-load uniqueness vs a per-day pattern based on what behavior you want long term. Based on learnings, this diverges from the date-based pattern used in the react-flow example but is still a reasonable choice here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35648d0 and 145f0ea.

📒 Files selected for processing (1)
  • examples/vanilla-codemirror6/src/main.ts (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-03-17T07:16:43.740Z
Learnt from: hackerwins
Repo: yorkie-team/yorkie-js-sdk PR: 954
File: examples/react-flow/src/main.tsx:44-59
Timestamp: 2025-03-17T07:16:43.740Z
Learning: In the react-flow example for yorkie-js-sdk, using a date-based document key (`react-flow-YYYYMMDD`) is an intentional design pattern to create a new document each day.

Applied to files:

  • examples/vanilla-codemirror6/src/main.ts
🧬 Code graph analysis (1)
examples/vanilla-codemirror6/src/main.ts (1)
examples/vanilla-codemirror6/src/type.ts (2)
  • YorkieDoc (3-5)
  • YorkiePresence (7-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (22.x)
🔇 Additional comments (2)
examples/vanilla-codemirror6/src/main.ts (2)

53-58: Undo/redo event routing through $.content subscription looks consistent

Routing operations when event.type === 'remote-change' or event.source === 'undoredo' and then applying them via handleOperations is a clean way to unify remote edits and history-driven changes, and skipping transactions annotated with Transaction.remote in the update listener avoids feedback loops. Just ensure that 'undoredo' matches the exact source string emitted by the history layer so local undo/redo always propagates correctly.

Also applies to: 66-80


148-172: syncText + snapshot handling cleanly resyncs the editor and restores selection

The new syncText helper that replaces the entire CM document from root.content.toString(), annotates the transaction as remote, and reconstructs the selection from getMyPresence().selection provides a robust way to handle snapshot events without corrupting local history or presence. This should keep the view aligned with Yorkie Text even after compaction/history operations.

@hackerwins hackerwins force-pushed the main branch 2 times, most recently from 2e116ac to 560f07d Compare December 4, 2025 07:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
examples/vanilla-codemirror6/src/main.ts (1)

122-123: Remove debug console.log statements before merging.

The console.log('undo') and console.log('redo') calls are debug artifacts that should be removed before this PR is merged.

       run: () => {
-        // To check undo works properly
-        console.log('undo');
         if (doc.history.canUndo()) {
       run: () => {
-        // To check redo works properly
-        console.log('redo');
         if (doc.history.canRedo()) {

Also applies to: 134-135

packages/sdk/src/document/operation/edit_operation.ts (1)

143-175: Attributes restoration limited to single-segment deletions.

As noted in a previous review, when toReverseOperation reverses a deletion spanning multiple text segments with different attributes, only the first segment's attributes are preserved (line 156-161). Multi-segment deletions with heterogeneous attributes will restore text content but apply only the first segment's attributes to the entire result.

This is a known limitation that should be documented or enhanced in a follow-up.

packages/sdk/src/document/crdt/rga_tree_split.ts (1)

828-853: Inconsistent length calculation for initial node in refinePos.

Line 838 uses getContentLength() (ignores removal status) for the initial node, but line 849 uses getLength() (returns 0 for removed nodes) for subsequent nodes. This violates the documented behavior (lines 812-813) stating: "Counts only live characters: removed nodes are treated as length 0."

When the initial node is removed, the current code incorrectly uses its full content length instead of treating it as 0.

Apply this diff to fix the inconsistency:

     let offsetInPart = pos.getRelativeOffset();
-    let partLen = node.getContentLength();
+    let partLen = node.getLength();

     while (offsetInPart > partLen) {
🧹 Nitpick comments (2)
packages/sdk/test/integration/history_text_test.ts (2)

9-50: Consider adding JSDoc for helper functions.

The static analysis tool flags missing JSDoc comments for applyTextOp1 and applyTextOp2. While these are test helpers, adding brief documentation would improve readability. Also, consider whether t.length ?? t.toString().length fallback is necessary—if Text always exposes .length, the fallback may be redundant.

+/**
+ * Applies a text operation to the document using position at end of text.
+ */
 function applyTextOp1(doc: Document<{ t: Text }>, op: TextOp) {

142-143: Use English for code comments.

The comment 텍스트 스냅샷 저장 (Korean for "save text snapshot") should be in English for codebase consistency.

-          // 텍스트 스냅샷 저장
+          // Save text snapshots
           const S: Array<string> = [];
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 145f0ea and 3bd7e20.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • examples/vanilla-codemirror6/package.json (1 hunks)
  • examples/vanilla-codemirror6/src/main.ts (5 hunks)
  • packages/sdk/src/document/change/change.ts (3 hunks)
  • packages/sdk/src/document/crdt/rga_tree_split.ts (3 hunks)
  • packages/sdk/src/document/crdt/text.ts (5 hunks)
  • packages/sdk/src/document/document.ts (4 hunks)
  • packages/sdk/src/document/history.ts (3 hunks)
  • packages/sdk/src/document/operation/edit_operation.ts (6 hunks)
  • packages/sdk/test/integration/history_text_test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/sdk/src/document/history.ts
  • examples/vanilla-codemirror6/package.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-03-17T07:16:43.740Z
Learnt from: hackerwins
Repo: yorkie-team/yorkie-js-sdk PR: 954
File: examples/react-flow/src/main.tsx:44-59
Timestamp: 2025-03-17T07:16:43.740Z
Learning: In the react-flow example for yorkie-js-sdk, using a date-based document key (`react-flow-YYYYMMDD`) is an intentional design pattern to create a new document each day.

Applied to files:

  • examples/vanilla-codemirror6/src/main.ts
🧬 Code graph analysis (5)
packages/sdk/src/document/crdt/rga_tree_split.ts (2)
packages/sdk/src/util/resource.ts (1)
  • DataSize (27-37)
packages/sdk/src/util/error.ts (1)
  • YorkieError (85-96)
packages/sdk/src/document/change/change.ts (3)
packages/sdk/src/api/yorkie/v1/resources_pb.ts (1)
  • Operation (199-264)
packages/sdk/src/document/operation/operation.ts (1)
  • OpInfo (40-45)
packages/sdk/src/document/history.ts (1)
  • HistoryOperation (28-33)
packages/sdk/test/integration/history_text_test.ts (2)
packages/sdk/src/document/document.ts (1)
  • Document (517-1976)
packages/sdk/test/integration/integration_helper.ts (1)
  • withTwoClientsAndDocuments (37-69)
packages/sdk/src/document/document.ts (1)
packages/sdk/src/document/operation/edit_operation.ts (1)
  • EditOperation (35-308)
packages/sdk/src/document/crdt/text.ts (3)
packages/sdk/src/document/crdt/gc.ts (1)
  • GCPair (24-27)
packages/sdk/src/util/resource.ts (1)
  • DataSize (27-37)
packages/sdk/src/document/crdt/rga_tree_split.ts (2)
  • RGATreeSplitPosRange (248-248)
  • RGATreeSplitPos (167-246)
🪛 ESLint
packages/sdk/test/integration/history_text_test.ts

[error] 9-10: Missing JSDoc comment.

(jsdoc/require-jsdoc)


[error] 52-53: Missing JSDoc comment.

(jsdoc/require-jsdoc)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (22.x)
🔇 Additional comments (25)
examples/vanilla-codemirror6/src/main.ts (3)

145-150: Good: Keymap ordering ensures custom undo/redo takes precedence.

Placing cmUndoRedoKeymap before basicSetup correctly ensures the custom undo/redo handlers override the default CodeMirror history extension.


153-177: LGTM!

The syncText function correctly handles snapshot synchronization by replacing editor content and restoring cursor position from presence. The Transaction.remote annotation properly prevents feedback loops.


187-200: The non-null assertion on op.value!.content is safe. The EditOpInfo type defines value as a required property, not optional, guaranteeing it is always present. No defensive check is needed.

Likely an incorrect or invalid review comment.

packages/sdk/src/document/document.ts (3)

86-86: LGTM!

The import is correctly added for the EditOperation type used in the reconciliation logic below.


1339-1339: LGTM!

Clearing the history after applying a snapshot is the correct approach, as the undo/redo stacks would reference stale positions that no longer exist in the new document state.


1355-1358: LGTM!

The clearHistory() helper method correctly clears both undo and redo stacks. This provides a clean API for resetting history state.

packages/sdk/test/integration/history_text_test.ts (4)

76-96: LGTM!

The single-operation undo tests correctly verify that each operation (insert, delete, replace) can be undone to restore the original text content.


125-169: LGTM!

The chained operations test comprehensively covers all 27 combinations (3³) of insert/delete/replace operations and verifies step-by-step undo correctness. Good use of snapshot array to track intermediate states.


171-216: LGTM!

The multi-client convergence tests properly sync between clients and verify that both documents converge after operations and undos. The triple sync pattern (c1.sync(), c2.sync(), c1.sync()) ensures full convergence.


218-261: LGTM!

The overlapping edits test is an important edge case for CRDT text undo. The test correctly verifies convergence after concurrent overlapping edits are both undone.

packages/sdk/src/document/change/change.ts (3)

157-161: LGTM!

The return type expansion to include operations: Array<Operation> is well-designed. This enables callers to access the actual executed operations for reconciliation purposes while maintaining backward compatibility with opInfos and reverseOps.


163-177: LGTM!

The implementation correctly collects only successfully executed operations (those that pass the !executionResult check) into changeOperations. This ensures that reconciliation only processes operations that actually modified the document state.


196-196: LGTM!

The return statement correctly includes all three components needed by the caller.

packages/sdk/src/document/operation/edit_operation.ts (5)

21-21: LGTM!

The CRDTTextValue import is correctly added to support the toReverseOperation method which processes removed values.


40-56: LGTM!

The isUndoOp field and constructor changes are clean. The field is optional and defaults to undefined, maintaining backward compatibility.


106-122: LGTM!

The undo operation handling correctly refines positions before editing and generates the reverse operation using the normalized from position. The refinement is necessary because the original positions may reference split nodes that have been modified.


177-202: LGTM!

The normalizePos method correctly validates the parent object type and returns normalized offsets. The implementation properly handles type checking and error cases.


207-261: Consider adding inline comments explaining each overlap scenario for improved maintainability.

The reconcileOperation method correctly handles six distinct overlap cases when reconciling undo/redo edit operations:

  1. No overlap (rangeTo <= a): Shift both positions by net change
  2. No overlap (b <= rangeFrom): No adjustment needed
  3. Fully contained (rangeFrom <= a && b <= rangeTo): Collapse to rangeFrom
  4. Contains the range (a <= rangeFrom && rangeTo <= b): Adjust end by net change
  5. Overlap at start (rangeFrom < a && a < rangeTo && rangeTo < b): Adjust both positions
  6. Overlap at end (a < rangeFrom && rangeFrom < b && b < rangeTo): Adjust end to rangeFrom

The method includes proper input validation and the logic is sound. However, adding brief inline comments above each conditional block (especially for cases 3-6) would significantly improve code clarity for future maintainers, given the complexity of the position calculations and the lack of direct unit test documentation.

packages/sdk/src/document/crdt/rga_tree_split.ts (2)

600-674: LGTM!

The expanded return type and removedValues collection are well-implemented. The removed values are correctly collected from the removed nodes, enabling the reverse operation to restore deleted content.


798-806: LGTM!

The normalizePos method correctly converts a local position to an absolute offset measured from the head of the chain. This enables position normalization for undo operations.

packages/sdk/src/document/crdt/text.ts (5)

29-29: LGTM: Import addition supports new position methods.

The addition of RGATreeSplitPos to the imports is necessary for the new position manipulation methods added later in the file.


257-274: Implementation correctly returns removed values for undo/redo.

The changes properly:

  • Capture removedValues from the underlying rgaTreeSplit.edit call
  • Map value changes to TextChange objects with parsed attributes and content
  • Return the expanded 5-tuple including the removed values

Note: The 4th element [caretPos, caretPos] creates a collapsed range (zero-length), which represents the cursor position after the edit.


405-410: New position normalization method added for undo/redo support.

The normalizePos method exposes position normalization at the CRDTText level, delegating to the underlying RGATreeSplit. This complements refinePos for undo/redo position handling.

Verify this method is used in the undo/redo implementation to ensure it's integrated as intended.


398-403: Position refinement method for undo/redo operations is actively used.

The refinePos method correctly exposes position refinement at the CRDTText API level and is used in the undo operation implementation (EditOperation.ts, lines 107-108) to refine fromPos and toPos coordinates. The underlying RGATreeSplit.refinePos handles position adjustments after node splits, which is essential for correctly replaying undo operations.


527-535: New position-to-index conversion method added to public CRDTText API.

The posToIndex method exposes position-to-index conversion as a public method in the CRDTText class, delegating to the underlying RGATreeSplit implementation. The preferToLeft parameter (defaulting to false) handles ambiguous position cases.

The method is currently unused within the codebase. Verify that this is intentional (e.g., for future features or external API consumers).

});
await client.activate();

const params = new URLSearchParams(window.location.search);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused params variable.

The params variable is created but never used in this file. This appears to be leftover code that should be cleaned up.

-  const params = new URLSearchParams(window.location.search);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = new URLSearchParams(window.location.search);
🤖 Prompt for AI Agents
In examples/vanilla-codemirror6/src/main.ts around line 23, remove the unused
variable declaration "const params = new
URLSearchParams(window.location.search);" since it's never referenced; simply
delete that line (or the declaration) to clean up leftover code and ensure there
are no linter warnings about unused variables.

Comment on lines +243 to +249
): [
Array<TextChange<A>>,
Array<GCPair>,
DataSize,
RGATreeSplitPosRange,
Array<CRDTTextValue>,
] {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the edit method in the file
sed -n '240,260p' packages/sdk/src/document/crdt/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 748


🏁 Script executed:

# Search for all calls to .edit( in TypeScript files
rg -n '\.edit\(' packages/sdk/src --type=ts -B 2 -A 5

Repository: yorkie-team/yorkie-js-sdk

Length of output: 4834


🏁 Script executed:

# Also search more broadly for potential edit method calls
rg -n 'edit\(' packages/sdk/src --type=ts | grep -v '//' | head -50

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1138


🏁 Script executed:

# Check the json/text.ts edit call more carefully
sed -n '110,125p' packages/sdk/src/document/json/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 528


🏁 Script executed:

# Check the CRDTTree.edit signature
sed -n '1103,1120p' packages/sdk/src/document/crdt/tree.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 674


🏁 Script executed:

# Check RGATreeSplit.edit signature
sed -n '602,630p' packages/sdk/src/document/crdt/rga_tree_split.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 788


🏁 Script executed:

# Check CRDTText.edit full signature and return type
sed -n '237,275p' packages/sdk/src/document/crdt/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1215


🏁 Script executed:

# Check what json/text.ts does with the returned values from edit
sed -n '100,135p' packages/sdk/src/document/json/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1039


🏁 Script executed:

# Check if there's any usage of removedValues that was expected to be captured
rg -n 'removedValues' packages/sdk/src/document/json/text.ts -B 5 -A 5

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


🏁 Script executed:

# Verify the json/text.ts class structure - does it use the 5th element?
sed -n '1,120p' packages/sdk/src/document/json/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 3869


🏁 Script executed:

# Double-check the exact position mapping - what is rangeAfterEdit getting assigned to
sed -n '116,125p' packages/sdk/src/document/json/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 253


🏁 Script executed:

# Check if removeValues should be used anywhere in the json/text.ts context
sed -n '88,140p' packages/sdk/src/document/json/text.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1410


🏁 Script executed:

# Verify what the old return type was - check git history or any documentation
# Let's search for any comments or type definitions that might indicate the old signature
rg -n 'RGATreeSplitPosRange|removedValues' packages/sdk/src/document/crdt/text.ts -B 3 -A 1

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1400


🏁 Script executed:

# Check if there are any tests that show the expected behavior
fd -t f 'text.test.ts|text.spec.ts' packages/sdk --exec grep -l "edit" {} \;

Repository: yorkie-team/yorkie-js-sdk

Length of output: 162


🏁 Script executed:

# Let's also check edit_operation to see how it uses the 5-tuple
sed -n '105,125p' packages/sdk/src/document/operation/edit_operation.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 583


Update all callers to destructure the expanded 5-tuple return type.

The CRDTText.edit() method now returns a 5-tuple instead of 4-tuple: [Array<TextChange<A>>, Array<GCPair>, DataSize, RGATreeSplitPosRange, Array<CRDTTextValue>]. The new 5th element contains removed values needed for undo/redo functionality.

Incomplete updates found:

  • packages/sdk/src/document/operation/edit_operation.ts:111 correctly captures all 5 elements with [changes, pairs, diff, , removedValues] and uses removedValues for reverse operations
  • packages/sdk/src/document/json/text.ts:116 only destructures 4 elements [, pairs, diff, rangeAfterEdit], silently ignoring the 5th element (removed values)

Update json/text.ts line 116 to capture the removed values, or explicitly acknowledge if they're intentionally unused at that layer.

🤖 Prompt for AI Agents
In packages/sdk/src/document/json/text.ts around line 116, the call to
CRDTText.edit is still destructuring only 4 elements and ignoring the new 5th
return value (removed values); update the destructuring to capture the fifth
element (for example: [, pairs, diff, rangeAfterEdit, removedValues] or [,
pairs, diff, rangeAfterEdit, _removedValues] if intentionally unused) and, if
the removed values are relevant at this layer, thread them into the JSON/text
undo/redo handling; otherwise explicitly name the unused variable with a leading
underscore to document intentional omission.

Comment on lines +1453 to +1469
const { opInfos, operations } = change.execute(
this.root,
this.presences,
source,
);
for (const op of operations) {
if (op instanceof EditOperation) {
const [from, to] = op.normalizePos(this.root);

this.internalHistory.reconcileTextEdit(
op.getParentCreatedAt(),
from,
to,
op.getContent()?.length ?? 0,
);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's test coverage for reconciliation when parent is removed
rg -n "reconcileTextEdit|normalizePos" --type ts packages/sdk/test -C 3 | head -100

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Find the normalizePos implementation
rg -n "normalizePos" --type ts packages/sdk/src -A 10 | head -80

Repository: yorkie-team/yorkie-js-sdk

Length of output: 6249


🏁 Script executed:

#!/bin/bash
# Check the change.execute method and surrounding error handling
rg -n "execute\(" --type ts packages/sdk/src/document/document.ts -B 2 -A 15 | head -100

Repository: yorkie-team/yorkie-js-sdk

Length of output: 2838


🏁 Script executed:

#!/bin/bash
# Look for error handling patterns around reconcileTextEdit
rg -n "reconcileTextEdit" --type ts packages/sdk/src -B 5 -A 10

Repository: yorkie-team/yorkie-js-sdk

Length of output: 2581


🏁 Script executed:

#!/bin/bash
# Search for test files that might test the reconciliation scenario
find packages/sdk/test -name "*.test.ts" -o -name "*.spec.ts" | head -20

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Look for any tests related to removed or deleted text operations
rg -n "EditOperation|removeNode|remove.*parent" --type ts packages/sdk/test -l | head -10

Repository: yorkie-team/yorkie-js-sdk

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Check if there's any try-catch around change.execute or in the method containing the code
sed -n '1440,1480p' packages/sdk/src/document/document.ts

Repository: yorkie-team/yorkie-js-sdk

Length of output: 1191


🏁 Script executed:

#!/bin/bash
# Look for broader error handling context in the notifyLocalChange method
rg -n "notifyLocalChange" --type ts packages/sdk/src/document/document.ts -A 50 | head -80

Repository: yorkie-team/yorkie-js-sdk

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Look at the Change.execute implementation to see if it validates parent existence
rg -n "class Change|execute\(" --type ts packages/sdk/src/document -A 5 | grep -A 20 "execute("

Repository: yorkie-team/yorkie-js-sdk

Length of output: 9409


🏁 Script executed:

#!/bin/bash
# Check if EditOperation.execute validates parent existence
rg -n "execute" --type ts packages/sdk/src/document/operation/edit_operation.ts -B 2 -A 15 | head -100

Repository: yorkie-team/yorkie-js-sdk

Length of output: 2058


Reconciliation correctly scopes to specific text instances, but normalizePos lacks defensive error handling.

The reconciliation logic properly uses the operation's parentCreatedAt to scope reconciliation to the correct text instance. However, if normalizePos throws (e.g., if the parent object was removed), the exception would propagate and potentially break the change application flow. While EditOperation.execute() already validates the parent exists before this code path runs, adding defensive error handling around normalizePos would improve robustness against edge cases or concurrent removals.

🤖 Prompt for AI Agents
In packages/sdk/src/document/document.ts around lines 1453 to 1469, normalizePos
may throw (e.g., parent removed) and currently would propagate and break change
application; wrap the call to op.normalizePos(this.root) in a try/catch, and on
error skip reconciliation for that operation (optionally log/debug the failure)
so the change application continues; only call internalHistory.reconcileTextEdit
when normalizePos returns successfully.

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.

3 participants