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

Implement Auth Token Update Interface for Auth Webhook #911

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

chacha912
Copy link
Contributor

@chacha912 chacha912 commented Oct 17, 2024

What this PR does / why we need it?

  • Modify client option to accept an authTokenInjector function instead of a static token
  • Implement retry logic after token update
  • Event notification on authentication error
    • client: activate, deactivate
    • document: attach, detach, pushpull, watch

Any background context you want to provide?

image

1. Token Refresh Support

Added a new authTokenInjector interface that enables token refresh based on authentication error messages.
When a 401(Unauthenticated) error occurs, the SDK calls the authTokenInjector with the error message to obtain a new authToken and retries the request.

// as-is
const client = new yorkie.Client(YorkieAPIAddr, {
  apiKey,
  token
});

// to-be
const client = new yorkie.Client(YorkieAPIAddr, {
  apiKey,
  authTokenInjector: async (authErrorMessage) => {
    // Handle token refresh logic according to the error message in the webhook response
    if (authErrorMessage === 'token expired') {
      return await refreshAccessToken();
    }
    return accessToken;
  },
});

2. Retry Configuration

Introduced new options to control retry behavior for failed authentication requests:

const client = new yorkie.Client(YorkieAPIAddr, {
  apiKey,
  authTokenInjector,
  retryRequestDelay: 2000, // milliseconds between retry attempts
  maxRequestRetries: 5,    // maximum number of retry attempts
});
  • Automatically retries requests with refreshed tokens on 401 errors
  • Configurable retry delay and maximum retry attempts (defaults to 3)
  • Prevents infinite retry loops with maximum retry limit

3. Updated Webhook Response Format

Modified the webhook response format: yorkie-team/yorkie#1037

  • as-is: {allowed: boolean, reason: string}
  • to-be: {code: number, message: string}

Supported response codes:

  • 200: OK (Allowed)
  • 401: Unauthenticated
  • 403: Permission Denied

403 errors and invalid webhook response formats will throw errors immediately without attempting token refresh.

4. Authentication Error Event Notifications

Added support for monitoring authentication errors through event subscriptions:

doc.subscribe('auth-error', (event) => {
  console.log(event.value); 
  // event.value contains:
  // - errorMessage: string
  // - method: 'AttachDocument' | 'DetachDocument' | 'PushPull' | 'WatchDocuments'
});
  • Notifies when a 401 response is received from the webhook
  • Provides error message and webhook method information
  • Currently available for document-related webhooks
  • Should we reintroduce the client.subscribe() structure for client-related webhook error notifications?

What are the relevant tickets?

Fixes yorkie-team/yorkie#996

Checklist

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

Summary by CodeRabbit

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced new authentication error handling with AuthError events.
    • Added errorMetadataOf function for improved error detail retrieval.
    • Enhanced Client class with token refresh functionality and retry logic for connection errors.
    • Added new dependencies, including axios, express, and @types/express, to support enhanced functionality.
  • Bug Fixes

    • Improved error handling during client synchronization and document management.
  • Tests

    • Added comprehensive integration tests for authentication webhooks.
    • Enhanced client tests to cover retry logic and error scenarios, including token refresh behavior.
  • Chores

    • Updated dependencies, including vite-tsconfig-paths, to improve development and build processes.

@chacha912 chacha912 marked this pull request as ready for review October 23, 2024 23:34
Copy link

coderabbitai bot commented Oct 23, 2024

Walkthrough

The pull request introduces several modifications across multiple files in the yorkie-js-sdk. Key changes include the addition of new dependencies in package.json, the introduction of a new public function for error metadata retrieval in converter.ts, and enhancements to the Client class for better token management and retry logic. Additionally, a new event type for authentication errors is added in the Document class, and new error codes are introduced in the error handling module. Integration tests for client synchronization and an authentication webhook have also been established.

Changes

File Change Summary
packages/sdk/package.json Added dependencies: @types/express, axios, express; updated vite-tsconfig-paths version.
packages/sdk/src/api/converter.ts Added method errorMetadataOf to retrieve error metadata from ConnectError.
packages/sdk/src/client/client.ts Updated ClientOptions to use authTokenInjector; added retry logic in activate, deactivate, attach, detach, and sync methods.
packages/sdk/src/document/document.ts Added AuthError event type and AuthErrorEvent interface; updated subscribe method for new event handling.
packages/sdk/src/util/error.ts Added error codes: ErrPermissionDenied, ErrUnauthenticated.
packages/sdk/test/helper/helper.ts Updated assertThrowsAsync function signature to accept `RegExp
packages/sdk/test/integration/client_test.ts Modified tests to include retryRequestDelay options; enhanced assertions for document management.
packages/sdk/test/integration/integration_helper.ts Added constants testAPIID and testAPIPW; modified Client instantiation with retry options.
packages/sdk/test/integration/webhook_test.ts Introduced integration tests for authentication webhook handling various token scenarios.

Assessment against linked issues

Objective Addressed Explanation
Provide a way to refresh token for Auth Webhook (#996)

Possibly related PRs

  • Unify error throwing methods #878: This PR modifies the src/api/converter.ts file, which is also affected in the main PR. Both PRs involve changes to error handling and function signatures in the same file, indicating a direct relationship.
  • Introducing version vector to solve GC problem #899: This PR introduces significant changes to the packages/sdk/src/api/converter.ts file, including new functions and modifications to existing ones. The main PR also involves changes to the converter.ts file, specifically in error handling and function updates, establishing a connection between the two.

Suggested reviewers

  • hackerwins
  • cloneot

Poem

🐰 In the meadow, changes bloom,
New tokens dance, dispelling gloom.
Errors caught in a gentle net,
With retries set, no need to fret.
A webhook's call, a test so bright,
In the SDK, all feels just right! 🌼


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>, please review it.
    • 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 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 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: 15

🧹 Outside diff range and nitpick comments (9)
packages/sdk/test/helper/helper.ts (1)

190-190: LGTM! Consider adding JSDoc examples.

The signature change to support both string and RegExp for error message validation is a good improvement. This will enable more flexible error testing patterns, especially useful for the new authentication token update feature.

Consider adding JSDoc examples to demonstrate both string and RegExp usage:

/**
 * Asserts that the async function throws an error matching the expected type and message.
 * @param fn The async function to test
 * @param errType The expected error type
 * @param message Optional string or RegExp to match against the error message
 * @example
 * // Using string message
 * await assertThrowsAsync(
 *   async () => { throw new Error('auth failed') },
 *   Error,
 *   'auth failed'
 * );
 * 
 * // Using RegExp pattern
 * await assertThrowsAsync(
 *   async () => { throw new Error('token expired: ABC123') },
 *   Error,
 *   /token expired: .+/
 * );
 */
packages/sdk/src/api/converter.ts (1)

822-832: Add JSDoc documentation for the new function.

The function lacks documentation about its purpose, parameters, return value, and usage examples. This is especially important as it's part of the authentication error handling mechanism.

Add comprehensive documentation:

+/**
+ * `errorMetadataOf` extracts all metadata from the given ConnectError.
+ * This is particularly useful for handling authentication errors and token refresh scenarios.
+ *
+ * @param error - The ConnectError containing error details
+ * @returns A record of metadata key-value pairs or an empty object if no metadata exists
+ * 
+ * @example
+ * ```typescript
+ * const error = new ConnectError("Auth failed");
+ * const metadata = errorMetadataOf(error);
+ * if (metadata.reason === "token_expired") {
+ *   // Handle token refresh
+ * }
+ * ```
+ */
 export function errorMetadataOf(error: ConnectError): Record<string, string> {
packages/sdk/src/document/document.ts (2)

909-917: Enhance documentation for auth-error subscription.

The method overload is correctly implemented, but the documentation could be more descriptive about when this event occurs and how to handle it.

 /**
  * `subscribe` registers a callback to subscribe to events on the document.
- * The callback will be called when the authentification error occurs.
+ * The callback will be called when authentication errors occur, such as:
+ * - Token expiration
+ * - Invalid token
+ * - Authentication webhook failures
+ * 
+ * @example
+ * ```typescript
+ * doc.subscribe('auth-error', (event) => {
+ *   console.log(`Auth error in ${event.value.method}: ${event.value.errorMessage}`);
+ *   // Handle token refresh
+ * });
+ * ```
  */

Line range hint 202-1101: Consider enhancing the auth error handling architecture.

The current implementation provides a good foundation for auth error handling through events. However, consider these architectural improvements:

  1. Error Recovery Pattern: Consider implementing a standardized error recovery pattern that includes:

    • Automatic retry with exponential backoff
    • Circuit breaker pattern for repeated auth failures
    • Queue for operations during token refresh
  2. Token Lifecycle Management: Consider adding events for the entire token lifecycle:

    • Token refresh initiated
    • Token refresh succeeded
    • Token refresh failed
  3. State Management: Consider tracking auth state to prevent operation attempts during token refresh.

This would make the authentication system more robust and provide better visibility into the auth state.

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

308-312: Avoid potential test flakiness due to timing dependencies

Using a short token expiration time like 500ms and relying on setTimeout can lead to flaky tests, especially on slower machines or under varying load conditions.

Consider increasing TokenExpirationMs to a higher value or mock the time functions to simulate token expiration without relying on real-time delays.

Also applies to: 328-329

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

Line range hint 428-436: Simplify the conditional statements in the broadcast event handler

Both conditions in the if and else if statements perform the same action. You can simplify the code by removing the conditional checks.

Apply this diff to simplify the code:

 const unsubscribe = d2.subscribe('broadcast', (event) => {
   const { topic, payload } = event.value;

-  if (topic === broadcastTopic1) {
-    eventCollector.add([topic, payload]);
-  } else if (topic === broadcastTopic2) {
-    eventCollector.add([topic, payload]);
-  }
+  eventCollector.add([topic, payload]);
 });
packages/sdk/src/client/client.ts (3)

132-138: Clarify the authErrorMessage parameter in authTokenInjector.

The description mentions an authError parameter, but the function signature uses authErrorMessage. Ensure consistency and clarify the purpose of this parameter in the documentation.


Line range hint 913-987: Ensure retryCount increments correctly in runWatchLoop.

In the runWatchLoop, the retryCount variable is initialized but may not increment in the error handling block, potentially leading to infinite retries. Increment retryCount upon each retry attempt and enforce maxRequestRetries.

Apply this diff to increment retryCount:

...
if (await this.handleConnectError(err)) {
  if (
    err instanceof ConnectError &&
    errorCodeOf(err) === Code.ErrUnauthenticated
  ) {
    if (retryCount >= this.maxRequestRetries) {
      logger.error(
        `[WD] c:"${this.getKey()}" max retries (${this.maxRequestRetries}) exceeded`,
      );
      reject(err);
      return;
    }
    attachment.doc.publish([
      {
        type: DocEventType.AuthError,
        value: {
          errorMessage: errorMetadataOf(err).message,
          method: 'WatchDocuments',
        },
      },
    ]);
  }
  onDisconnect();
+ retryCount++;
} else {
  this.conditions[ClientCondition.WatchLoop] = false;
}
...

Line range hint 1147-1186: Expand retry logic to include additional error codes.

Currently, handleConnectError retries on a specific set of error codes. Consider including DeadlineExceeded and Internal errors, which are often transient and may benefit from retrying.

Apply this diff to handle more retryable errors:

...
if (
  err.code === ConnectErrorCode.Canceled ||
  err.code === ConnectErrorCode.Unknown ||
  err.code === ConnectErrorCode.ResourceExhausted ||
  err.code === ConnectErrorCode.Unavailable ||
+ err.code === ConnectErrorCode.DeadlineExceeded ||
+ err.code === ConnectErrorCode.Internal
) {
  return true;
}
...
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 1d8adb3 and 840cf00.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • packages/sdk/package.json (2 hunks)
  • packages/sdk/src/api/converter.ts (1 hunks)
  • packages/sdk/src/client/client.ts (21 hunks)
  • packages/sdk/src/document/document.ts (6 hunks)
  • packages/sdk/src/util/error.ts (1 hunks)
  • packages/sdk/test/helper/helper.ts (1 hunks)
  • packages/sdk/test/integration/client_test.ts (1 hunks)
  • packages/sdk/test/integration/integration_helper.ts (2 hunks)
  • packages/sdk/test/integration/webhook_test.ts (1 hunks)
🔇 Additional comments (10)
packages/sdk/test/integration/integration_helper.ts (2)

31-36: Verify retry logic test coverage.

Setting retryRequestDelay: 0 could potentially mask timing-related issues in the retry mechanism. While this makes tests run faster, it might not catch real-world scenarios where delays between retries are crucial.

#!/bin/bash
# Check for retry-related test coverage
echo "Checking for retry-related tests..."
rg -g '**/*test.ts' -B 5 -A 5 'retryRequestDelay|retry.*logic|retry.*mechanism'

# Look for other test files that might need the retry configuration
echo "Checking for other client initializations that might need updating..."
rg -g '**/*test.ts' 'new yorkie\.Client'

Consider adding:

  1. Tests with non-zero retry delays to validate timing behavior
  2. Tests specifically for the retry mechanism with token updates
  3. Tests for maximum retry attempts and failure scenarios

8-9: Consider security implications of default credentials and add documentation.

While using default credentials in tests is common, having them default to 'admin' could lead to:

  1. Accidentally running tests against production if env vars are misconfigured
  2. Security issues if these defaults are copied to other contexts

Consider these improvements:

  1. Add JSDoc comments explaining the purpose and usage of these constants
  2. Use more obvious test credentials (e.g., 'test-api-id') to prevent confusion
  3. Consider making these required env vars for better security
✅ Verification successful

Let me generate additional verification scripts to check for security implications.

The first script will help us verify that these test credentials are indeed confined to test files. The second script will help us understand the broader authentication testing patterns in the codebase to make a more informed decision about the security implications of these default credentials.


Let me try one more verification with corrected pattern for test files:

This should help us understand the authentication patterns in tests and check if these credentials are mentioned in any documentation, which would be concerning from a security perspective.


Based on the verification results, I can now provide a final response:

Default test credentials are safely confined to test environment

The verification shows that:

  1. The TEST_API_ID and TEST_API_PW environment variables and their default values are only used in test files
  2. The credentials are part of a comprehensive authentication testing suite that includes proper token management, expiration handling, and error scenarios
  3. These credentials are not referenced in any documentation or example files that could lead to misuse

The default 'admin' values are acceptable in this context because:

  1. They are only used in integration tests
  2. The test suite properly tests both environment variable and default scenarios
  3. The authentication system has proper security measures including token expiration and refresh mechanisms
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if these credentials are used in actual implementation code
rg -g '!test/**' -l 'admin.*admin' .

Length of output: 17785


Script:

#!/bin/bash
# Check if these credentials are used in production code or configuration
rg -g '!test/**' 'TEST_API_ID|TEST_API_PW' .

# Check if there are any other test credentials in the test files
rg -g 'test/**' 'apiKey|token|credential|auth' -B 2 -A 2

Length of output: 484


Script:

#!/bin/bash
# Check for authentication patterns in test files
rg -g '**/*test*.ts' 'apiKey|token|credential|auth' -B 2 -A 2

# Check if these test credentials are referenced in any documentation or example files
rg -g '**/*.{md,mdx,txt}' 'TEST_API_ID|TEST_API_PW|testAPIID|testAPIPW'

Length of output: 24484

packages/sdk/package.json (2)

67-67: LGTM: Safe dependency update.

The update of vite-tsconfig-paths from 4.2.1 to 4.3.1 is a minor version bump that should maintain backward compatibility according to semver.


56-56: Verify security implications of axios version.

The current axios version (1.7.7) might have known vulnerabilities. Consider upgrading to the latest patch version.

Let's check for known vulnerabilities:

✅ Verification successful

Let me try a different approach to verify the axios version security implications using GitHub's API to check for security advisories.

This script will help us:

  1. Check for any published security advisories affecting axios
  2. Get the latest version from the main branch to compare with 1.7.7

Let me try one final verification approach using the GitHub API to check for security advisories in a different way.


Axios version 1.7.7 is secure - no action needed

Based on the GitHub Security Advisory data, version 1.7.7 is not affected by any known vulnerabilities:

  • The most recent high-severity vulnerability affects versions <= 1.7.3
  • Other vulnerabilities affect older version ranges that don't include 1.7.7

The current version (1.7.7) is actually newer than the versions affected by known security issues, making it a secure choice.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for known vulnerabilities in axios v1.7.7
# Note: This is a basic check. In production, you should use tools like npm audit

# Get the latest version of axios 1.7.x
LATEST=$(npm view axios versions --json | jq -r '.[]' | grep '^1\.7\.' | sort -V | tail -n 1)
echo "Current version: 1.7.7"
echo "Latest 1.7.x version: $LATEST"

if [ "$LATEST" != "1.7.7" ]; then
  echo "Consider upgrading to $LATEST"
fi

Length of output: 360


Script:

#!/bin/bash
# Check GitHub Security Advisories for axios vulnerabilities
gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/axios/axios/security-advisories \
  | jq '.[] | select(.state=="published") | {published_at, summary, severity, references: .references.advisory_url}'

# Also check the latest version from the package.json in axios repo
curl -s https://raw.githubusercontent.com/axios/axios/main/package.json | jq -r '.version'

Length of output: 339


Script:

#!/bin/bash
# Check for vulnerabilities using GitHub's security advisory database
gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  graphql -f query='
    query {
      securityVulnerabilities(ecosystem: NPM, package: "axios", first: 5, orderBy: {field: UPDATED_AT, direction: DESC}) {
        nodes {
          severity
          vulnerableVersionRange
          advisory {
            publishedAt
            summary
          }
        }
      }
    }
  '

Length of output: 1335

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

202-206: LGTM! New auth error event type is well-defined.

The addition of AuthError event type follows consistent naming patterns and is properly documented.


448-448: LGTM! Callback mapping is correctly defined.

The 'auth-error' event callback mapping follows the established pattern and uses the correct type.


1090-1101: LGTM! Auth error event handling is properly implemented.

The implementation follows the established pattern for event handling and is clean and straightforward.

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

400-405: Ensure proper error handling in asynchronous operations

The client.attach(doc) operation is asynchronous and may throw errors. Without proper error handling or assertions, failures might go unnoticed, potentially leading to silent test failures.

Ensure that all asynchronous operations are wrapped in try-catch blocks or are properly awaited with assertions to handle any potential errors.

await client.attach(doc);
+ // Add assertion or error handling as needed
packages/sdk/test/integration/client_test.ts (2)

Line range hint 332-340: No issues found with the synchronization test

The added code correctly tests the behavior of clients in RealtimePushOnly sync mode and ensures events are handled as expected.


Line range hint 612-614: Test for 'maxRetries' set to zero works as expected

The added test correctly verifies that broadcasting does not retry on network failure when maxRetries is set to zero.

packages/sdk/package.json Outdated Show resolved Hide resolved
packages/sdk/src/util/error.ts Show resolved Hide resolved
packages/sdk/src/api/converter.ts Outdated Show resolved Hide resolved
packages/sdk/src/document/document.ts Outdated Show resolved Hide resolved
packages/sdk/test/integration/webhook_test.ts Show resolved Hide resolved
packages/sdk/src/client/client.ts Show resolved Hide resolved
packages/sdk/src/client/client.ts Show resolved Hide resolved
packages/sdk/src/client/client.ts Show resolved Hide resolved
packages/sdk/src/client/client.ts Show resolved Hide resolved
Comment on lines +1166 to +1186
// NOTE(chacha912): If the error is `Unauthenticated`, it means that the
// token is invalid or expired. In this case, the client gets a new token
// from the `authTokenInjector` and retries the api call.
if (errorCodeOf(err) === Code.ErrUnauthenticated) {
const token =
this.authTokenInjector &&
(await this.authTokenInjector(errorMetadataOf(err).message));

this.rpcClient = createPromiseClient(
YorkieService,
createGrpcWebTransport({
baseUrl: this.rpcAddr,
interceptors: [
createAuthInterceptor(this.apiKey, token),
createMetricInterceptor(),
],
}),
);
return true;
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid recreating RPC client on authentication errors.

Recreating the RPC client upon each Unauthenticated error may be inefficient. Instead, consider updating the interceptor with the new token to reuse the existing client.

Apply this diff to update the interceptor:

...
if (errorCodeOf(err) === Code.ErrUnauthenticated) {
  const token =
    this.authTokenInjector &&
    (await this.authTokenInjector(errorMetadataOf(err).message));

- this.rpcClient = createPromiseClient(
-   YorkieService,
-   createGrpcWebTransport({
-     baseUrl: this.rpcAddr,
-     interceptors: [
-       createAuthInterceptor(this.apiKey, token),
-       createMetricInterceptor(),
-     ],
-   }),
- );
+ if (this.rpcClient) {
+   this.rpcClient.transport.interceptors = [
+     createAuthInterceptor(this.apiKey, token),
+     createMetricInterceptor(),
+   ];
+ }
  return true;
}
...

Committable suggestion was skipped due to low confidence.

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

🧹 Outside diff range and nitpick comments (2)
packages/sdk/src/client/client.ts (1)

1063-1071: Consider adding a comment explaining the race condition.

The code handles a race condition between syncLoop and syncMode, but the comment could be more explicit about why this check is necessary and what problems it prevents.

-// NOTE(chacha912, hackerwins): If syncLoop already executed with
-// PushPull, ignore the response when the syncMode is PushOnly.
+// NOTE(chacha912, hackerwins): Ignore the response when in PushOnly mode to prevent
+// applying changes twice. This can happen when syncLoop has already executed with
+// PushPull mode and the mode was subsequently changed to PushOnly. Without this
+// check, the same changes could be applied multiple times.
packages/sdk/src/api/converter.ts (1)

807-818: Consider improving type safety and documentation.

The function implementation looks good, but could benefit from these improvements:

  1. Consider using a type for metadata keys to ensure type safety.
  2. Add JSDoc to document the return type structure and possible metadata keys.
+/**
+ * Returns the error metadata from the given ConnectError.
+ * @param error - The ConnectError instance
+ * @returns Record<MetadataKey, string> where MetadataKey can be 'code' or other error-specific keys
+ */
 export function errorMetadataOf(error: ConnectError): Record<string, string> {
   // NOTE(chacha912): Currently, we only use the first detail to represent the
   // error metadata.
   const infos = error.findDetails(ErrorInfo);
   for (const info of infos) {
     return info.metadata;
   }

   return {};
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 840cf00 and 0cbdff2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/sdk/package.json (2 hunks)
  • packages/sdk/src/api/converter.ts (1 hunks)
  • packages/sdk/src/client/client.ts (21 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/package.json
🔇 Additional comments (3)
packages/sdk/src/client/client.ts (2)

132-136: LGTM! Well-documented token injector interface.

The authTokenInjector interface is well-designed with clear documentation explaining its purpose and behavior.


1080-1084: LGTM! Good handling of removed documents.

The code properly handles cleanup of watch streams for removed documents, preventing unnecessary network traffic and potential memory leaks.

packages/sdk/src/api/converter.ts (1)

820-824: LGTM! Good refactoring.

The function has been nicely refactored to use errorMetadataOf, making it more maintainable and reducing code duplication.

@yorkie-team yorkie-team deleted a comment from coderabbitai bot Oct 24, 2024
@yorkie-team yorkie-team deleted a comment from coderabbitai bot Oct 24, 2024
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: 2

🧹 Outside diff range and nitpick comments (1)
packages/sdk/src/client/client.ts (1)

Line range hint 894-978: Reset retry count on successful watch stream connection.

The retry count for watch loop is not reset when the connection is successfully established. This could lead to premature rejection of future reconnection attempts if the connection is lost after several retries.

Add retry count reset after successful connection:

 if (resp.body.case === 'initialization') {
+  retryCount = 0;  // Reset retry count on successful connection
   resolve([stream, ac]);
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 0cbdff2 and acd5ca6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/sdk/package.json (2 hunks)
  • packages/sdk/src/api/converter.ts (1 hunks)
  • packages/sdk/src/client/client.ts (21 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/package.json
🔇 Additional comments (3)
packages/sdk/src/client/client.ts (1)

Line range hint 132-172: LGTM! Well-documented interface changes.

The new options are clearly documented and properly typed:

  • authTokenInjector for dynamic token updates
  • retryRequestDelay and maxRequestRetries for configuring retry behavior
packages/sdk/src/api/converter.ts (2)

809-822: Consider extracting shared error detail logic.

The function implementation looks good, but there's an opportunity to reduce code duplication with errorCodeOf as suggested in the past review.


824-828: LGTM!

The refactored implementation is more concise and properly handles undefined values.

Comment on lines +282 to +311
const requestActivateClient = async (retryCount = 0): Promise<void> => {
return this.rpcClient!.activateClient(
{ clientKey: this.key },
{ headers: { 'x-shard-key': this.apiKey } },
)
.then((res) => {
this.id = res.clientId;
this.status = ClientStatus.Activated;
this.runSyncLoop();
.then((res) => {
this.id = res.clientId;
this.status = ClientStatus.Activated;
this.runSyncLoop();
logger.info(`[AC] c:"${this.getKey()}" activated, id:"${this.id}`);
})
.catch(async (err) => {
if (retryCount >= this.maxRequestRetries) {
logger.error(
`[AC] c:"${this.getKey()}" max retries (${
this.maxRequestRetries
}) exceeded`,
);
throw err;
}

logger.info(`[AC] c:"${this.getKey()}" activated, id:"${this.id}"`);
})
.catch((err) => {
logger.error(`[AC] c:"${this.getKey()}" err :`, err);
this.handleConnectError(err);
throw err;
});
if (await this.handleConnectError(err)) {
await delay(this.retryRequestDelay);
return requestActivateClient(retryCount + 1);
}

logger.error(`[AC] c:"${this.getKey()}" err :`, err);
throw err;
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider extracting common retry logic to a utility function.

The retry logic is duplicated across multiple methods (activate, deactivate, attach, detach, sync). Consider extracting it to a reusable utility function to improve maintainability.

Create a utility function that encapsulates the retry logic:

private async withRetry<T>(
  operation: (retryCount: number) => Promise<T>,
  context: {
    method: string,
    doc?: Document<any, any>,
    logPrefix: string
  }
): Promise<T> {
  const requestWithRetry = async (retryCount = 0): Promise<T> => {
    try {
      return await operation(retryCount);
    } catch (err) {
      if (retryCount >= this.maxRequestRetries) {
        logger.error(
          `[${context.logPrefix}] c:"${this.getKey()}" max retries (${
            this.maxRequestRetries
          }) exceeded`
        );
        throw err;
      }

      if (await this.handleConnectError(err)) {
        if (errorCodeOf(err) === Code.ErrUnauthenticated && context.doc) {
          context.doc.publish([
            {
              type: DocEventType.AuthError,
              value: {
                errorMessage: errorMetadataOf(err).message,
                method: context.method,
              },
            },
          ]);
        }
        await delay(this.retryRequestDelay);
        return requestWithRetry(retryCount + 1);
      }

      logger.error(`[${context.logPrefix}] c:"${this.getKey()}" err :`, err);
      throw err;
    }
  };

  return requestWithRetry();
}

Example usage:

return this.withRetry(
  async (retryCount) => {
    return this.rpcClient!.activateClient(
      { clientKey: this.key },
      { headers: { 'x-shard-key': this.apiKey } }
    ).then((res) => {
      this.id = res.clientId;
      this.status = ClientStatus.Activated;
      this.runSyncLoop();
      logger.info(`[AC] c:"${this.getKey()}" activated, id:"${this.id}`);
    });
  },
  { method: 'ActivateClient', logPrefix: 'AC' }
);

Also applies to: 325-352, 402-483, 518-573, 1039-1127

packages/sdk/src/api/converter.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

🧹 Outside diff range and nitpick comments (2)
packages/sdk/src/api/converter.ts (2)

807-809: Enhance function documentation.

The documentation could be more descriptive about the function's behavior, especially regarding error handling and the structure of returned metadata.

 /**
- * `errorMetadataOf` returns the error metadata of the given connect error.
+ * `errorMetadataOf` returns the metadata from the first ErrorInfo detail of the given connect error.
+ * If the input is not a ConnectError or no ErrorInfo details are found, returns an empty object.
+ * @param error - The ConnectError to extract metadata from
+ * @returns A record of string key-value pairs containing error metadata
  */

807-828: Well-aligned with Auth Token Update requirements.

The enhanced error handling implementation aligns well with the PR objectives for the Auth Token Update Interface. The ability to extract detailed error metadata will be particularly useful for handling authentication-related errors and token refresh scenarios.

Consider documenting common error metadata fields that will be used for authentication scenarios, particularly those related to token expiration and refresh requirements.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between acd5ca6 and 95f5faa.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/sdk/package.json (2 hunks)
  • packages/sdk/src/api/converter.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/package.json
🔇 Additional comments (1)
packages/sdk/src/api/converter.ts (1)

824-828: LGTM! Good refactoring.

The function has been effectively refactored to reuse errorMetadataOf, addressing the code duplication issue from previous reviews.

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 b05d0be and 8f34257.

📒 Files selected for processing (1)
  • packages/sdk/src/document/document.ts (6 hunks)
🔇 Additional comments (5)
packages/sdk/src/document/document.ts (5)

202-206: LGTM! The new DocEventType enum value is well-defined.

The AuthError enum value follows the established naming pattern and has a clear descriptive comment.


227-228: LGTM! The DocEvent type union is correctly updated.

The AuthErrorEvent is properly added to the DocEvent type union.


451-451: LGTM! The DocEventCallbackMap is correctly updated.

The 'auth-error' mapping is properly added with the correct callback type.


1093-1104: LGTM! The subscribe implementation is well-structured.

The implementation correctly follows the established pattern for event subscriptions.


424-430: 🛠️ Refactor suggestion

Consider enhancing AuthErrorEvent with additional error details.

The current interface could be improved to better handle token expiration scenarios and provide more context for debugging:

The previous review suggestion is still valid. Apply this diff to enhance error information:

 export interface AuthErrorEvent extends BaseDocEvent {
   type: DocEventType.AuthError;
   value: {
     errorMessage: string;
     method: 'AttachDocument' | 'DetachDocument' | 'PushPull' | 'WatchDocuments';
+    code?: string;
+    details?: Record<string, unknown>;
   };
 }

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

Provide a way to refresh token for Auth Webhook
1 participant