Skip to content
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

Introduce broadcast API for event sharing #884

Merged
merged 21 commits into from
Sep 3, 2024

Conversation

gwbaik9717
Copy link
Contributor

@gwbaik9717 gwbaik9717 commented Aug 21, 2024

What this PR does / why we need it?

This PR implements a broadcast API, which enables the sharing of a broader range of general events beyond the current document and presence events in Yorkie's Publish-Subscribe model.

Any background context you want to provide?

1. Broadcast Events:

Users can now broadcast custom events with a specified topic and payload.
The payload can be of any type, as long as it is serializable.

// Broadcast an event with a topic and payload
const payload = 'hello';
doc.broadcast('TOPIC_NAME', payload);

2. Subscribe to Broadcast Events:

Users can subscribe to specific topics and handle the events via a callback function.
The callback is triggered whenever an event with the corresponding topic is broadcast.

// Subscribe to a specific topic for broadcast events
doc.subscribe('broadcast', ({value: {topic, payload, clientID}}) => {
    // Handle the broadcast event for the specified topic
});

What are the relevant tickets?

Related to yorkie-team/yorkie#628

Checklist

  • Added relevant tests or not required
  • Didn't break anything

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a broadcast method in the Client class to send messages to specified topics.
    • Added new event types Broadcast and LocalBroadcast to enable clients to subscribe to broadcast messages.
    • Implemented a method to validate the serializability of payloads for broadcasting.
    • Enhanced the Document class with a subscribe method for clients to register handlers for broadcast events.
  • Bug Fixes

    • Improved error handling for broadcasting unserializable payloads within the Client class.
  • Tests

    • Added test cases to verify successful broadcasting of serializable payloads and error handling for invalid payloads.
    • Increased test coverage for event subscriptions related to broadcast events.

@gwbaik9717 gwbaik9717 requested a review from sejongk August 21, 2024 01:17
Copy link

coderabbitai bot commented Aug 21, 2024

Walkthrough

The recent changes introduce a broadcasting feature in the Client and Document classes, allowing clients to subscribe to broadcast events. New event types, Broadcast and LocalBroadcast, are added, along with corresponding interfaces and methods for managing subscriptions and broadcasting payloads with validation. Comprehensive test cases validate the new functionality, ensuring robust error handling and event management.

Changes

Files Change Summary
packages/sdk/src/client/client.ts Added broadcast method in Client class for payload broadcasting with validation and error handling.
packages/sdk/src/document/document.ts Introduced Broadcast and LocalBroadcast event types, BroadcastEvent and LocalBroadcastEvent interfaces, and methods for subscribing to broadcast events. Updated event handling logic.
packages/sdk/test/integration/client_test.ts Added test cases for successful broadcasting of serializable payloads and error handling for unserializable payloads, enhancing test coverage.

Poem

In the meadow where bytes play,
A broadcast hops and bounds today.
With messages shared, both near and far,
Events flutter like a shining star.
So let us dance, both rabbit and friend,
With joy in code, our fun won't end! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

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>.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @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.

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

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 2f1cd8b and 323f981.

Files selected for processing (4)
  • packages/sdk/src/client/client.ts (2 hunks)
  • packages/sdk/src/document/document.ts (9 hunks)
  • packages/sdk/src/util/validator.ts (1 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Additional comments not posted (8)
packages/sdk/src/util/validator.ts (1)

20-30: LGTM! The validateSerializable function is well-implemented.

The function correctly checks if a value is serializable using JSON.stringify and handles exceptions appropriately.

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

591-639: LGTM! The broadcast method is well-implemented.

The method includes necessary checks for client activation, document attachment, and payload serializability. Error handling and logging are appropriately managed.

Run the following script to verify the usage of the broadcast method in the codebase:

Verification successful

Verification Complete: broadcast Method Usage

The broadcast method is implemented correctly and is tested for error handling with unserializable payloads. No other direct usages were found in the codebase, indicating its usage is limited or indirect. The review comment accurately reflects the method's implementation.

  • The method is tested in packages/sdk/test/integration/client_test.ts.
Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify all function calls to `broadcast` match the expected usage.

# Test: Search for the function usage. Expect: Correct usage of the `broadcast` method.
rg --type javascript -A 5 $'broadcast'

Length of output: 72


Script:

#!/bin/bash
# Description: Verify all function calls to `broadcast` match the expected usage.

# Corrected command to search for the function usage.
rg 'broadcast' --glob '*.ts' -A 5

Length of output: 7095

packages/sdk/test/integration/client_test.ts (1)

867-885: LGTM! The test case for broadcasting unserializable payloads is well-implemented.

The test case effectively verifies that the broadcast method throws an error when provided with an unserializable payload.

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

178-180: LGTM! The addition of the Broadcast event type is well-integrated.

This enhancement allows for broadcast messaging capabilities within the document's event system.


380-383: LGTM! The BroadcastEvent interface is well-defined.

The interface correctly extends BaseDocEvent and includes the necessary properties for broadcast events.


603-611: LGTM! The broadcastEventHandlers map is well-initialized.

This map effectively stores handlers for broadcast events, indexed by their topic.


1650-1656: LGTM! The handling of Broadcast events in applyDocEvent is well-implemented.

This ensures that broadcast events are processed and the appropriate handlers are invoked.


1052-1079: LGTM! The subscribeBroadcastEvent method is well-implemented.

This method allows clients to register callbacks for broadcast events and manage subscriptions effectively.

Run the following script to verify the usage of the Broadcast event type in the codebase:

Verification successful

Broadcast Event Handling Verified

The usage of DocEventType.Broadcast in the codebase is consistent and correctly implemented for handling broadcast events. The subscribeBroadcastEvent method and related code appropriately manage event subscriptions and callbacks. No issues were found.

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify all usages of the `Broadcast` event type.

# Test: Search for the `Broadcast` event type usage. Expect: Correct implementation and handling.
rg --type javascript -A 5 $'DocEventType.Broadcast'

Length of output: 85


Script:

#!/bin/bash
# Description: Verify all usages of the `Broadcast` event type.

# Test: Search for the `Broadcast` event type usage. Expect: Correct implementation and handling.
rg -A 5 'DocEventType.Broadcast'

Length of output: 1652

@gwbaik9717
Copy link
Contributor Author

@sejongk

There are a few things I'd like to share:

1. How Handlers for Each Topic Are Managed:

Inside the Document class, I've added a broadcastEventHandlers which is a map to manage the handlers for each topic.

// document.ts 
export class Document<T, P extends Indexable = Indexable> {
  // skipped

  private broadcastEventHandlers: Map<
    string,
    (topic: string, payload: any) => void
  >;
}

When a user subscribes to a specific topic for broadcast events:

// User subscribes to a broadcast event with the topic "TOPIC_NAME"
doc.subscribeBroadcastEvent('TOPIC_NAME', (topic, payload) => {
    // Handle the broadcast event for the specified topic
});

The handler is registered in the broadcastEventHandlers map:

export class Document<T, P extends Indexable = Indexable> {
  
  public subscribeBroadcastEvent(
    topic: string,
    handler: (topic: string, payload: any) => void,
    error?: ErrorFn,
  ): Unsubscribe {
  
    // Register the handler in broadcastEventHandlers 
    this.broadcastEventHandlers.set(topic, handler);

2. How PbDocEventType is Converted to DocEventType:

Within the applyWatchStream method of the Document class, which applies the given watch stream response to the document, the conversion from PbDocEventType to DocEventType occurs as follows:

  public applyWatchStream(resp: WatchDocumentResponse) {
      // skipped
      } else if (type === PbDocEventType.DOCUMENT_BROADCAST) {
        if (resp.body.value.body) {
          const { topic, payload } = resp.body.value.body;
          const decoder = new TextDecoder();

          event.push({
            type: DocEventType.Broadcast,
            value: { topic, payload: JSON.parse(decoder.decode(payload)) },
          });
        }
      }

      if (event.length > 0) {
        this.publish(event);
      }
    }
  }

3. Test Case for Sending Unserializable Payloads:

As discussed earlier, I've added a test case in client_test.ts to handle unserializable payloads. If there are any additional test cases needed, please let me know.

Lastly, I've verified that the broadcast API works as expected in several examples, but I believe further testing is necessary to ensure complete coverage.

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 323f981 and b692441.

Files selected for processing (1)
  • packages/sdk/src/document/document.ts (9 hunks)
Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/src/document/document.ts

Copy link
Contributor

@sejongk sejongk left a comment

Choose a reason for hiding this comment

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

Overall, it looks good to me 👍
I've left some comments.

packages/sdk/test/integration/client_test.ts Outdated Show resolved Hide resolved
packages/sdk/src/document/document.ts Outdated Show resolved Hide resolved
packages/sdk/src/client/client.ts Outdated Show resolved Hide resolved
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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between b692441 and 66c996a.

Files selected for processing (1)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/test/integration/client_test.ts

@hackerwins
Copy link
Member

@gwbaik9717
CI failed due to incorrect pnpm version in CI configuration. It is currently fixed in the main branch.

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 66c996a and b0b2766.

Files selected for processing (2)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
  • packages/sdk/test/integration/integration_helper.ts (2 hunks)
Additional comments not posted (6)
packages/sdk/test/integration/integration_helper.ts (1)

24-24: LGTM! Verify the usage of withTwoClientsAndDocuments.

The introduction of the syncMode parameter enhances flexibility. Ensure that all calls to withTwoClientsAndDocuments are updated to use the new parameter if necessary.

Run the following script to verify the function usage:

Also applies to: 35-36

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

867-884: LGTM! Test for serializable payload broadcasting.

The test case for broadcasting a serializable payload is well-implemented and ensures that no errors are thrown.


886-904: LGTM! Test for error handling with unserializable payloads.

The test case correctly verifies that broadcasting an unserializable payload throws an error.


907-932: LGTM! Test for triggering subscribed broadcast event handlers.

The test case effectively verifies that a subscribed handler is triggered for broadcast events.


934-959: LGTM! Test for unsubscribed broadcast event handlers.

The test case correctly ensures that handlers for unsubscribed events are not triggered.


961-991: LGTM! Test for unsubscribing from broadcast events.

The test case accurately verifies that handlers are not triggered after unsubscribing.

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between b0b2766 and 8060985.

Files selected for processing (4)
  • packages/sdk/src/client/attachment.ts (1 hunks)
  • packages/sdk/src/client/client.ts (4 hunks)
  • packages/sdk/src/document/document.ts (16 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Additional context used
GitHub Check: build (18.x)
packages/sdk/src/document/document.ts

[failure] 629-629:
Don't use null as a type. Use undefined instead of null


[failure] 1341-1341:
Missing JSDoc comment


[failure] 1341-1341:
Don't use null as a type. Use undefined instead of null


[failure] 2090-2090:
Unnecessary try/catch wrapper

Biome
packages/sdk/src/document/document.ts

[error] 2093-2093: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

Additional comments not posted (17)
packages/sdk/src/client/attachment.ts (1)

111-116: LGTM!

The function is correctly implemented.

The code changes are approved.

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

589-640: LGTM!

The function is well-implemented with proper validation and error handling.

The code changes are approved.

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

867-882: LGTM!

The test case is correctly implemented and covers the expected behavior.

The code changes are approved.


884-902: LGTM!

The test case is correctly implemented and covers the expected behavior.

The code changes are approved.


905-933: LGTM!

The test case is correctly implemented and covers the expected behavior.

The code changes are approved.


935-963: LGTM!

The test case is correctly implemented and covers the expected behavior.

The code changes are approved.


965-998: LGTM!

The test case is correctly implemented and covers the expected behavior.

The code changes are approved.

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

177-181: LGTM!

The addition of the Broadcast event type is appropriate and aligns with the new broadcast functionality.

The code changes are approved.


200-201: LGTM!

The update to include BroadcastEvent in the DocEvent type is necessary and correctly implemented.

The code changes are approved.


381-384: LGTM!

The BroadcastEvent interface is well-defined and aligns with the requirements for broadcasting events.

The code changes are approved.


403-403: LGTM!

The addition of the broadcast callback in DocEventCallbackMap is necessary and correctly implemented.

The code changes are approved.


565-568: LGTM!

The BroadcastSubscribePair type is well-defined and aligns with the requirements for subscribing to broadcast events.

The code changes are approved.


620-627: LGTM!

The addition of broadcastEventHandlers is necessary and correctly implemented to manage handlers for broadcast events.

The code changes are approved.


658-659: LGTM!

The initialization of broadcastEventHandlers in the constructor is necessary and correctly implemented.

The code changes are approved.


862-873: LGTM!

The new overload of the subscribe method is necessary and correctly implemented to support subscribing to broadcast events.

The code changes are approved.


Line range hint 1563-1600: LGTM!

The update to handle broadcast events in the applyWatchStream method is necessary and correctly implemented.

The code changes are approved.


1691-1697: LGTM!

The update to handle broadcast events in the applyDocEvent method is necessary and correctly implemented.

The code changes are approved.

packages/sdk/src/document/document.ts Outdated Show resolved Hide resolved
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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 8060985 and 84e8762.

Files selected for processing (2)
  • packages/sdk/src/client/attachment.ts (1 hunks)
  • packages/sdk/src/document/document.ts (16 hunks)
Files skipped from review as they are similar to previous changes (2)
  • packages/sdk/src/client/attachment.ts
  • packages/sdk/src/document/document.ts

@gwbaik9717
Copy link
Contributor Author

@sejongk

Here is an update.

1. Unified Broadcast Event Subscription

Users can now use the existing subscribe method to subscribe to broadcast events. Since subscribing to a broadcast event requires both a type and a topic, I've introduced a new SubscribePair type, which is structured as follows:

type SubscribePair = {
  type: string;
};

Building on this, the BroadcastSubscribePair extends SubscribePair:

type BroadcastSubscribePair = {
  type: 'broadcast';
  topic: string;
} & SubscribePair;

Here’s an example of how to subscribe to a broadcast event:

   const unsubscribe = d2.subscribe(
        { type: 'broadcast', topic: "TOPIC_NAME" },
        (topic, payload) => {},
   );

2. Call broadcast from Document not Client

Broadcasting is now handled directly by the Document not Client, aligning with the subscription mechanism.

To support this, I’ve added a client attribute within the Document class. This client attribute is an instance of the Client and is set when the client attaches to the document, and unset when it detaches.

export class Document<T, P extends Indexable = Indexable> {
  // skipped
  private client?: Client;
  
    /**
   * `setClient` sets the client of this document.
   *
   * @internal
   */
  public setClient(client?: Client): void {
    this.client = client;
  }

}

Here’s an example of how to broadcast a topic with payload.

// must be serializable
const payload = {a:1, b:"2"}
await doc.broadcast("TOPIC_NAME", payload);

Copy link
Contributor

@sejongk sejongk left a comment

Choose a reason for hiding this comment

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

LGTM 👍.
@hackerwins Is there anything else to review?

@hackerwins
Copy link
Member

@sejongk

Sorry for the late review. I'll check it today.

CC) @chacha912, @gwbaik9717

Copy link
Contributor

@chacha912 chacha912 left a comment

Choose a reason for hiding this comment

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

@gwbaik9717 Thank you for the PR, and I apologize for the delayed review. I've left a few comments for your consideration.

packages/sdk/src/document/document.ts Outdated Show resolved Hide resolved
packages/sdk/src/document/document.ts Outdated Show resolved Hide resolved
packages/sdk/test/integration/client_test.ts Outdated Show resolved Hide resolved
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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 84e8762 and 69650c0.

Files selected for processing (3)
  • packages/sdk/src/client/attachment.ts (1 hunks)
  • packages/sdk/src/document/document.ts (16 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Files skipped from review as they are similar to previous changes (2)
  • packages/sdk/src/client/attachment.ts
  • packages/sdk/src/document/document.ts
Additional comments not posted (5)
packages/sdk/test/integration/client_test.ts (5)

867-882: LGTM!

The test case correctly verifies that the broadcast method can handle a valid payload.

The code changes are approved.


884-902: LGTM!

The test case correctly verifies the error handling of the broadcast method when given an unserializable payload.

The code changes are approved.


905-928: LGTM!

The test case correctly verifies that a subscribed broadcast event handler is triggered correctly.

The code changes are approved.


930-958: LGTM!

The test case correctly verifies that an unsubscribed event handler does not trigger.

The code changes are approved.


960-993: LGTM!

The test case correctly verifies that a handler does not trigger after unsubscribing.

The code changes are approved.

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 41da599 and 5ac5e2b.

Files selected for processing (2)
  • packages/sdk/src/document/document.ts (13 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/src/document/document.ts
Additional comments not posted (5)
packages/sdk/test/integration/client_test.ts (5)

867-882: LGTM!

The test case correctly verifies that broadcasting a serializable payload does not throw any errors.

The code changes are approved.


884-902: LGTM!

The test case correctly verifies that broadcasting an unserializable payload throws the expected error.

The code changes are approved.


904-929: LGTM!

The test case correctly verifies that the handler is triggered for a subscribed broadcast event.

The code changes are approved.


931-962: LGTM!

The test case correctly verifies that the handler is not triggered for an unsubscribed broadcast event.

The code changes are approved.


964-997: LGTM!

The test case correctly verifies that the handler is not triggered for a broadcast event after unsubscribing.

The code changes are approved.

@hackerwins hackerwins self-requested a review August 29, 2024 05:17
Refactor to have Client subscribe to Document's broadcast events and handle them by calling Client's broadcast method
This commit handles the case when broadcast event fails such as when trying to broadcast unserializable payload
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

Outside diff range, codebase verification and nitpick comments (2)
packages/sdk/src/document/document.ts (2)

380-384: Consider using a more specific type for the payload property.

The BroadcastEvent interface is well-defined and aligns with the new broadcast functionality. However, the payload type is set to any, which could be improved for better type safety. Consider using a more specific type that represents JSON-serializable data.


2023-2034: Consider using a more specific type for the payload parameter.

The broadcast method is well-implemented and aligns with the new broadcast functionality. However, the payload type is set to any, which could be improved for better type safety. Consider using a more specific type that represents JSON-serializable data.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 5ac5e2b and e88e3c1.

Files selected for processing (4)
  • packages/sdk/src/client/attachment.ts (2 hunks)
  • packages/sdk/src/client/client.ts (5 hunks)
  • packages/sdk/src/document/document.ts (10 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Additional comments not posted (15)
packages/sdk/src/client/attachment.ts (2)

3-3: LGTM!

The import statement for Unsubscribe is correct and necessary.


25-26: LGTM!

The addition of the unsubscribeBroadcastEvent property and the updated constructor are correctly implemented and enhance the class's functionality.

Also applies to: 32-39

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

45-45: LGTM!

The import statement for validateSerializable is correct and necessary.


307-318: LGTM!

The addition of the unsubscribeBroacastEvent parameter in the attach method is correctly implemented and necessary for managing broadcast event subscriptions.

Also applies to: 345-345


601-652: LGTM!

The addition of the broadcast method is correctly implemented and enhances the Client class's functionality by allowing the broadcasting of payloads to specified topics.


818-818: LGTM!

The call to unsubscribeBroadcastEvent in the detachInternal method is correctly implemented and necessary for managing broadcast event subscriptions.

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

867-882: LGTM!

The test case for successfully broadcasting a serializable payload is correctly implemented and necessary for validating the broadcast functionality.


884-906: LGTM!

The test case for broadcasting an unserializable payload is correctly implemented and necessary for validating the error handling of the broadcast functionality.


908-933: LGTM!

The test case for triggering the handler for a subscribed broadcast event is correctly implemented and necessary for validating the event subscription functionality.


935-966: LGTM!

The test case for not triggering the handler for an unsubscribed broadcast event is correctly implemented and necessary for validating the event unsubscription functionality.


968-1002: LGTM!

The test case for not triggering the handler for a broadcast event after unsubscribing is correctly implemented and necessary for validating the event unsubscription functionality.

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

176-180: LGTM!

The addition of the Broadcast event type in the DocEventType enum is straightforward and aligns with the new broadcast functionality.


199-200: LGTM!

The addition of the BroadcastEvent type to the DocEvent union type is necessary for the new broadcast functionality.


403-403: LGTM!

The addition of the broadcast callback to the DocEventCallbackMap type is necessary for the new broadcast functionality.


834-842: LGTM!

The addition of the subscribe method overload for the broadcast event type is necessary for the new broadcast functionality.

@gwbaik9717
Copy link
Contributor Author

@hackerwins @chacha912 @sejongk

Here's an update.

In order to resolve the circular reference issue, we’ve removed the client reference from the Document class. Instead, we've decided to have Client subscribe to Document's broadcast events and handle them by calling Client's broadcast method.

However, this solution presents two challenges:

1. Unsubscribing Handlers

We had to ensure that handlers registered in subscribe are properly unsubscribed when document is detached from client.
To address this, we’ve decided to manage unsubscribeBroadcastEvent within the Attachment class, since the lifecycle of the client’s attachment and the sub/unsub process are the same.

export class Attachment<T, P extends Indexable> {
  // skipped
  unsubscribeBroadcastEvent: Unsubscribe;
}

With this approach, when attaching a document to a client using the attach method of Client, we subscribe to the document’s event stream, and we unsubscribe when detaching:

  public attach<T, P extends Indexable>(
    doc: Document<T, P>,
    options: {
      initialPresence?: P;
      syncMode?: SyncMode;
    } = {},
  ): Promise<Document<T, P>> {
    // skipped
    const unsubscribeBroacastEvent = doc.subscribe(
      'broadcast',
      (topic, payload, onBroadcastError) => {
        try {
          this.broadcast(doc.getKey(), topic, payload);
        } catch (e: unknown) {
          if (e instanceof Error) {
            onBroadcastError?.(e);
          }
        }
      },
    );
    
    this.attachmentMap.set(
          doc.getKey(),
          new Attachment(
            this.reconnectStreamDelay,
            doc,
            res.documentId,
            syncMode,
            unsubscribeBroacastEvent,
         ),
    );
  private detachInternal(docKey: DocumentKey) {
    // skipped
    attachment.unsubscribeBroadcastEvent();

2. Error Handling for Broadcast Events

The second challenge involves handling errors during broadcast events, such as when broadcasting an unserializable payload. Initially, we could simply use a try-catch block to handle errors when directly accessing the client's broadcast method:

try {
   doc.broadcast(broadcastTopic, payload)
}catch(e){
 // do something
}

However, since we’re now dealing with an event stream, a different approach to error handling is required. To accommodate this, I've introduced a new interface for broadcasting events. This interface includes an error handling function that triggers when a broadcast event fails.

// Usage
const errorFn = () => {
    // Do something
}
doc.broadcast("topic", payload, errorFn)

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

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between e88e3c1 and 43ae22c.

Files selected for processing (1)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Additional comments not posted (6)
packages/sdk/test/integration/client_test.ts (6)

867-882: LGTM!

The test case correctly verifies that broadcasting a serializable payload does not throw an error.


884-908: LGTM!

The test case correctly verifies that broadcasting an unserializable payload throws an error.


910-935: LGTM!

The test case correctly verifies that the handler is triggered for a subscribed broadcast event.


937-968: LGTM!

The test case correctly verifies that the handler is not triggered for an unsubscribed broadcast event.


970-1004: LGTM!

The test case correctly verifies that the handler is not triggered for a broadcast event after unsubscribing.


866-866: LGTM!

The test case correctly verifies that local changes are handled correctly when receiving a snapshot.

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

Outside diff range, codebase verification and nitpick comments (1)
packages/sdk/src/document/document.ts (1)

Line range hint 1540-1581: LGTM! But consider adding error handling for JSON parsing.

The code changes are approved. However, consider adding error handling for the JSON.parse call to handle potential parsing errors gracefully.

You can wrap the JSON.parse call in a try-catch block to catch any parsing errors:

  event.push({
    type: DocEventType.Broadcast,
    value: {
      clientID: publisher,
      topic,
-     payload: JSON.parse(decoder.decode(payload)),      
+     payload: (() => {
+       try {
+         return JSON.parse(decoder.decode(payload));
+       } catch (e) {
+         console.error('Failed to parse broadcast payload:', e);
+         return null;
+       }  
+     })(),
    },
  });
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 43ae22c and ffe525f.

Files selected for processing (3)
  • packages/sdk/src/client/client.ts (5 hunks)
  • packages/sdk/src/document/document.ts (10 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
Additional comments not posted (17)
packages/sdk/src/client/client.ts (4)

607-655: LGTM!

The new broadcast method in the Client class looks well implemented. It has proper checks for active client and attached document, validates the payload serializability, enqueues the broadcast task, encodes payload as JSON, and includes robust error handling.


348-348: LGTM!

The code change to pass unsubscribeBroacastEvent to the Attachment constructor is approved.


307-321: LGTM!

The new code to subscribe to document's 'local-broadcast' events and broadcast them using the client's broadcast method looks good. It correctly extracts the topic and payload, calls the broadcast method, and passes any errors to the event's error function.


821-821: LGTM!

Calling attachment.unsubscribeBroadcastEvent() when a document is detached in the detachInternal method is a good change. It will prevent issues from subscriptions to a detached document.

packages/sdk/test/integration/client_test.ts (6)

867-882: LGTM!

The "Should successfully broadcast serializeable payload" test looks good. It correctly tests the happy path of broadcasting a serializable payload without errors.


884-908: LGTM!

The "Should throw error when broadcasting unserializeable payload" test looks good. It correctly tests the scenario of broadcasting an unserializable payload and expects the right error message.


910-939: LGTM!

The "Should trigger the handler for a subscribed broadcast event" test looks good. It correctly tests that a subscribed broadcast handler is triggered with the right topic and payload when a broadcast event is received.


941-974: LGTM!

The "Should not trigger the handler for an unsubscribed broadcast event" test looks good. It correctly tests that a broadcast handler subscribed to a specific topic is not triggered when a broadcast event with a different topic is received.


976-1012: LGTM!

The "Should not trigger the handler for a broadcast event after unsubscribing" test looks good. It correctly tests that a broadcast handler is not triggered after it has been unsubscribed, even if a broadcast event is received.


1014-1058: LGTM!

The "Should not trigger the handler for a broadcast event sent by the publisher to itself" test looks good. It correctly tests that a broadcast event sent by a document does not trigger its own handler, but triggers handlers on other subscribed documents.

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

176-185: LGTM!

The code changes are approved.


204-206: LGTM!

The code changes are approved.


386-396: LGTM!

The code changes are approved.


415-416: LGTM!

The code changes are approved.


847-864: LGTM!

The code changes are approved.


1013-1036: LGTM!

The code changes are approved.


2057-2069: LGTM!

The code changes are approved.

@gwbaik9717
Copy link
Contributor Author

gwbaik9717 commented Sep 1, 2024

Here's an update.

As mentioned earlier, we've decided to have the Client subscribe to the Document's broadcast events and handle them by invoking the Client's broadcast method. The challenge we encountered was distinguishing between local and remote broadcast events. Since the client is now subscribed to the document's broadcast events, both local and remote events are received, which creates the following issue:

image

Without differentiation, this setup can lead to an unexpected infinite loop. This happens because the client, upon receiving its own broadcast event, will rebroadcast it, creating a never-ending cycle.

image

This scenario can be reproduced with the following test code. The test simulates a situation where the publisher is broadcasting an event and, at the same time, subscribing to the broadcast event. Currently, since Yorkie does not support self-broadcast filtering, the subscribe handler gets called, leading to numerous recursive broadcasts, as evidenced by the test results:

 it('Should not trigger the handler for a broadcast event sent by the publisher to itself', async ({
    task,
  }) => {
    await withTwoClientsAndDocuments<{ t: Text }>(
      async (c1, d1, c2, d2) => {
        const eventCollector1 = new EventCollector<[string, any]>();
        const eventCollector2 = new EventCollector<[string, any]>();
        const broadcastTopic = 'test';
        const payload = { a: 1, b: '2' };

        // Publisher subscribes to the broadcast event
        const unsubscribe1 = d1.subscribe('broadcast', (topic, payload) => {
          if (topic === broadcastTopic) {
            eventCollector1.add([topic, payload]);
          }
        });

        const unsubscribe2 = d2.subscribe('broadcast', (topic, payload) => {
          if (topic === broadcastTopic) {
            eventCollector2.add([topic, payload]);
          }
        });

        d1.broadcast(broadcastTopic, payload);

        // Assuming that subscribers can receive the broadcast event within 1000ms.
        await new Promise((res) => setTimeout(res, 1000));

        unsubscribe1();
        unsubscribe2();

        assert.equal(eventCollector1.getLength(), 0);
        assert.equal(eventCollector2.getLength(), 1);
      },
      task.name,
      SyncMode.Realtime,
    );
  });
Screenshot 2024-09-01 at 12 00 34 PM

 

To resolve this issue, I've introduced a LocalBroadcast event type in addition to the existing Broadcast event. The LocalBroadcast event is only triggered when the local document attempts to broadcast. This addition allows us to clearly distinguish between local and remote broadcast events, preventing the infinite loop scenario by ensuring that a client does not handle its own broadcast events.
 

export enum DocEventType {
  /**
   * `Broadcast` means that the broadcast event is received from the remote client.
   */
  Broadcast = 'broadcast',

  /**
   * `LocalBroadcast` means that the broadcast event is sent from the local client.
   */
  LocalBroadcast = 'local-broadcast',
}
 /**
   * `broadcast` the payload to the given topic.
   */
  public broadcast(topic: string, payload: any, error?: ErrorFn) {
    const broadcastEvent: LocalBroadcastEvent = {
      type: DocEventType.LocalBroadcast,
      value: { topic, payload },
      error,
    };

    this.publish([broadcastEvent]);
  }

However, the above method still has a drawback. We may want to hide the LocalBroadcast event from users, as it is primarily intended for internal handling rather than user-facing logic.

doc.subscribe('local-broadcast', (event) => {
    // Users do not need to know about this event
},
);

Copy link
Member

@hackerwins hackerwins left a comment

Choose a reason for hiding this comment

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

@gwbaik9717 @sejongk @chacha912
Thanks for your contribution and review.

@hackerwins hackerwins merged commit 2b2ee0b into yorkie-team:main Sep 3, 2024
2 checks passed
hackerwins added a commit that referenced this pull request Sep 12, 2024
To prevent serialization issues, this commit narrows the type of the `presence`
object `P`. Previously, `<P extends Indexable>` allowed any value type, 
including non-JSON serializable ones like byte arrays, Date, and Long.

We now introduce a `Json` type, inspired by Liveblocks, to ensure only JSON
serializable types are allowed in the presence object. This change affects
functions like `broadcast`, ensuring safe serialization and addressing 
issues such as #884.

The new `Json` type is defined as follows:

```
export type Json = JsonPrimitive | JsonArray | JsonObject;
type JsonPrimitive = string | number | boolean | null;
type JsonArray = Array<Json>;
type JsonObject = { [key: string]: Json | undefined };
```

This type restriction enhances type safety and prevents potential runtime 
errors during JSON serialization of presence data.

---------

Co-authored-by: Youngteac Hong <susukang98@gmail.com>
JOOHOJANG pushed a commit that referenced this pull request Oct 22, 2024
To prevent serialization issues, this commit narrows the type of the `presence`
object `P`. Previously, `<P extends Indexable>` allowed any value type, 
including non-JSON serializable ones like byte arrays, Date, and Long.

We now introduce a `Json` type, inspired by Liveblocks, to ensure only JSON
serializable types are allowed in the presence object. This change affects
functions like `broadcast`, ensuring safe serialization and addressing 
issues such as #884.

The new `Json` type is defined as follows:

```
export type Json = JsonPrimitive | JsonArray | JsonObject;
type JsonPrimitive = string | number | boolean | null;
type JsonArray = Array<Json>;
type JsonObject = { [key: string]: Json | undefined };
```

This type restriction enhances type safety and prevents potential runtime 
errors during JSON serialization of presence data.

---------

Co-authored-by: Youngteac Hong <susukang98@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants