Skip to content

Add support for document re-attachment with same client #996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

minwoo1999
Copy link

@minwoo1999 minwoo1999 commented May 29, 2025

What this PR does / why we need it

This PR improves the developer experience when using yorkie-js-sdk by allowing a previously detached document to be reattached using the same client instance.

In some use cases—such as apps that need to switch between documents while maintaining a single active client—it’s natural to detach one document and later reattach it. However, this flow currently results in an error, as the internal state of the document isn't reset upon detachment.

To address this, we expose a public reset() method that clears the document’s internal state, making it ready for reattachment. This allows developers to reattach previously detached documents safely and explicitly.

Background context

Here's a typical scenario:

  • A persistent client is created and activated.
  • A doc1 is attached and then detached.
  • A second doc2 is attached and detached.
  • Attempting to reattach doc1 causes an error unless its internal state is reset.

By calling doc1.reset() before reattaching, this issue is resolved.

Test

image

What are the relevant tickets?

Fixes yorkie-team/yorkie#1353

Reported issue: Reattaching a detached document with the same client causes failure.

Checklist

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

Summary by CodeRabbit

  • New Features

    • Added the ability to reset a document's state, enabling seamless detachment and re-attachment with the same client.
  • Tests

    • Introduced an integration test confirming that a document can be detached and re-attached with the same client while preserving content and status.

Copy link

coderabbitai bot commented May 29, 2025

Walkthrough

The changes introduce a new reset() method to the Document class, invoked during the Client's detach operation. This method resets the document's internal state, enabling the same document instance to be re-attached to the client after detachment. An integration test is added to verify this re-attachment behavior.

Changes

File(s) Change Summary
packages/sdk/src/client/client.ts Modified Client.detach to call doc.reset() after updating document status.
packages/sdk/src/document/document.ts Added public reset() method and supporting private helpers to clear document state.
packages/sdk/test/integration/document_test.ts Added integration test for re-attaching a detached document with the same client instance.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client
    participant Document
    participant Server

    User->>Client: attach(Document)
    Client->>Server: Attach request
    Server-->>Client: Attach response
    Client->>Document: Set status to Attached

    User->>Document: Update content
    User->>Client: sync()
    Client->>Server: Sync changes
    Server-->>Client: Sync response

    User->>Client: detach(Document)
    Client->>Document: Set status to Detached
    Client->>Document: reset()

    User->>Client: attach(Document)
    Client->>Server: Attach request
    Server-->>Client: Attach response
    Client->>Document: Set status to Attached
Loading

Assessment against linked issues

Objective Addressed Explanation
Maintain client instance while detaching and re-attaching the same document (#939)
Allow re-attachment of a previously detached document instance with the same client (#939)

Possibly related PRs

  • Remove reattach test code #855: Addresses document detachment and reattachment logic, closely related to the reset functionality and re-attachment support.

Poem

A document hops, then takes a rest,
With reset() it’s soon at its best.
Detach, re-attach—no need to fear,
The same client welcomes it back here.
With every sync and gentle test,
Yorkie’s code just passed the quest!
🐇✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/sdk/src/document/document.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".

(The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "/packages/sdk".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install @typescript-eslint/eslint-plugin@latest --save-dev

The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in "packages/sdk/.eslintrc.js » ../../.eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

packages/sdk/src/client/client.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".

(The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "/packages/sdk".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install @typescript-eslint/eslint-plugin@latest --save-dev

The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in "packages/sdk/.eslintrc.js » ../../.eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

packages/sdk/test/integration/document_test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".

(The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "/packages/sdk".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install @typescript-eslint/eslint-plugin@latest --save-dev

The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in "packages/sdk/.eslintrc.js » ../../.eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.


📜 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 40b4347 and 7458f46.

📒 Files selected for processing (3)
  • packages/sdk/src/client/client.ts (1 hunks)
  • packages/sdk/src/document/document.ts (1 hunks)
  • packages/sdk/test/integration/document_test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/sdk/src/client/client.ts
  • packages/sdk/src/document/document.ts
  • packages/sdk/test/integration/document_test.ts
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@CLAassistant
Copy link

CLAassistant commented May 29, 2025

CLA assistant check
All committers have signed the CLA.

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: 1

🧹 Nitpick comments (3)
packages/sdk/test/integration/document_test.ts (3)

949-953: Consider removing redundant sync operation.

The sync operation on line 950 might be redundant since the document was already synced on line 945. Consider whether this additional sync is necessary for the test scenario.

    // 2. Detach document and sync before detaching
-   await client.sync();
    await client.detach(doc);
    assert.equal(doc.getStatus(), DocStatus.Detached);

970-975: Consider removing redundant sync operation in cleanup.

The sync operation on line 971 might be redundant since the document was already synced on line 967. Consider whether this additional sync is necessary for cleanup.

    // 6. Final cleanup
-   await client.sync();
    await client.detach(doc);
    assert.equal(doc.getStatus(), DocStatus.Detached);
    await client.deactivate();

927-976: Consider adding explicit verification of the reset functionality.

While this integration test effectively verifies the end-to-end re-attachment behavior, it doesn't explicitly test that the reset() method is being called or verify its effects. Consider adding assertions or logging to confirm that the document's internal state is properly reset during detachment.

This could help ensure that the test is specifically validating the new reset functionality rather than just testing that re-attachment works by coincidence.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a4f33c and 65af7c1.

📒 Files selected for processing (3)
  • packages/sdk/src/client/client.ts (1 hunks)
  • packages/sdk/src/document/document.ts (1 hunks)
  • packages/sdk/test/integration/document_test.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/sdk/test/integration/document_test.ts (2)
packages/sdk/test/integration/integration_helper.ts (2)
  • toDocKey (30-35)
  • testRPCAddr (8-8)
packages/sdk/src/yorkie.ts (2)
  • SyncMode (29-29)
  • DocStatus (45-45)
packages/sdk/src/document/document.ts (4)
packages/sdk/src/document/change/change_id.ts (1)
  • InitialChangeID (260-265)
packages/sdk/src/document/change/checkpoint.ts (1)
  • InitialCheckpoint (108-108)
packages/sdk/src/document/crdt/root.ts (1)
  • CRDTRoot (78-393)
packages/sdk/src/yorkie.ts (1)
  • DocStatus (45-45)
🔇 Additional comments (9)
packages/sdk/src/document/document.ts (4)

694-698: LGTM! Clean reset of change tracking state.

The method correctly resets all change tracking state to initial values using the appropriate constants. This ensures a clean slate for document reattachment.


700-703: LGTM! Proper CRDT state reset.

The method correctly resets the CRDT state by creating a fresh root using CRDTRoot.create() and clearing any existing clone. This ensures the document's data structure is properly reinitialized.


705-708: LGTM! Comprehensive client state reset.

The method correctly clears all client-related state including online clients and presence information. This is essential for clean document reattachment as presence data is session-specific.


710-723: LGTM! Well-designed comprehensive reset method.

The method provides a complete reset by orchestrating all state clearing operations. The @internal annotation correctly indicates this is not part of the public API, and the comprehensive JSDoc documentation clearly explains its purpose for document reattachment.

packages/sdk/src/client/client.ts (1)

543-543: LGTM! Correct integration of document reset functionality.

The doc.reset() call is properly placed after setting the document status to detached and only for non-removed documents. This ensures the document's internal state is cleared and ready for potential reattachment, which addresses the original issue described in the PR objectives.

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

927-939: LGTM: Test setup follows established patterns.

The test setup is well-structured with proper type definitions, unique document key generation, and client initialization.


940-948: LGTM: First attachment cycle is comprehensive.

The initial attach-update-sync cycle properly verifies both document status and content, establishing a good baseline for the re-attachment test.


957-962: LGTM: Re-attachment verification is thorough.

The re-attachment process properly verifies that the document can be attached again and maintains its previous content state.


963-969: LGTM: Post-reattachment functionality verification.

The test correctly verifies that the document remains fully functional after re-attachment by performing updates and confirming the changes persist.

Comment on lines +954 to +956
// 3. Wait for server state to settle
await new Promise((resolve) => setTimeout(resolve, 500));

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace arbitrary delay with more robust waiting mechanism.

The 500ms delay appears to be a workaround for server state synchronization. Consider using a more robust approach such as polling the server state or using explicit synchronization mechanisms rather than relying on an arbitrary timeout.

This approach could be brittle in different environments (slower CI systems, network latency, etc.) and may cause flaky tests.

-   // 3. Wait for server state to settle
-   await new Promise((resolve) => setTimeout(resolve, 500));
+   // 3. Ensure server state is consistent (if needed)
+   // Note: The reset() method should handle internal state cleanup

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/sdk/test/integration/document_test.ts around lines 954 to 956,
replace the fixed 500ms timeout used to wait for server state synchronization
with a more reliable method. Implement a polling loop or use explicit
synchronization APIs to repeatedly check the server state until it reaches the
expected condition or a timeout occurs, ensuring the test waits precisely for
readiness instead of an arbitrary delay. This will make the test more stable and
less prone to flakiness in varying environments.

@krapie
Copy link
Member

krapie commented May 29, 2025

Hello @minwoo1999!
Could you please sign our Contributor License Agreement (CLA) so we can proceed with the review?

Copy link

codecov bot commented May 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.59%. Comparing base (9a4f33c) to head (7458f46).
⚠️ Report is 24 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #996      +/-   ##
==========================================
+ Coverage   78.47%   78.59%   +0.12%     
==========================================
  Files          65       65              
  Lines        5626     5639      +13     
  Branches     1025     1025              
==========================================
+ Hits         4415     4432      +17     
+ Misses        907      905       -2     
+ Partials      304      302       -2     

☔ 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.

@minwoo1999
Copy link
Author

Hello @minwoo1999! Could you please sign our Contributor License Agreement (CLA) so we can proceed with the review?

Hello, I've signed the Contributor License Agreement (CLA). Please let me know if there's anything else needed!

@krapie krapie requested review from krapie and removed request for krapie June 2, 2025 12:51
@krapie
Copy link
Member

krapie commented Jun 2, 2025

@hackerwins @chacha912 Why are we seeing this kind of behavior? I'm not sure about the context of the "document already detached" error when reattaching a document on the same client.

@blurfx blurfx self-requested a review July 1, 2025 06:57
@hackerwins
Copy link
Member

hackerwins commented Jul 1, 2025

@krapie @minwoo1999 Thanks for your contribution.

The state of a document attached to a client(ClientDocInfo) originally followed this state transition model:

 ┌──────────┐ Attach ┌──────────┐ Remove ┌─────────┐  
 │ Detached ├───────►│ Attached ├───────►│ Removed │  
 └──────────┘        └─┬─┬──────┘        └─────────┘  
           ▲           │ │     ▲  
           └───────────┘ └─────┘  
              Detach     PushPull  

However, when a client detaches a document, the document may become stale and subject to garbage collection.

GC in Yorkie removes CRDT tombstones based on the versionvectors of AttachedClients. If a detached document is not referenced by any other replica, it may retain tombstone nodes that are no longer accessible, leading to inconsistencies.

To prevent this, we currently disallow reattaching a document after it has been detached(yorkie-team/yorkie#904).
While this avoids the GC issue, it also limits the natural usage pattern of attaching and detaching documents.

To improve this, we're considering allowing clients to reattach previously detached documents—but only by resetting them to a clean, initial state before attachment. This would avoid resurrecting stale tombstone data while restoring flexibility to users.

Since this behavior involves both SDK and server state handling, it would be best implemented in the Yorkie main repo (Go) first and reflect it to other SDKs.

https://github.com/yorkie-team/yorkie/blob/7b32d2ba7b2406250f58c91384efa5cd6959c3ef/test/integration/document_test.go#L80-L102

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.

Can't change attached document maintaining client
4 participants