From 5fc446e5882aef7938a0ae1e1df3ee31be8b7a92 Mon Sep 17 00:00:00 2001 From: israx <70438514+israx@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:13:50 -0500 Subject: [PATCH] chore(auth): debounce refreshAuthTokens (#12845) * chore: add debounce callback helper * chore: add unit tests * chore: debounce fetchAuthSession * chore: fix unit test * chore: fix bundle size limits * chore: update debounce logic * chore: update dedup logic * chore: debounce refreshAuthTokens * chore: fix bundle size * chore: address feedback * chore: fix unit test * chore: address feedback * chore: update yarn.lock * chore: address feedbak --- .../cognito/utils/refreshAuthTokens.ts | 5 +- packages/aws-amplify/package.json | 16 +++--- .../utils/deDupeAsyncRequests.test.ts | 50 +++++++++++++++++++ packages/core/src/libraryUtils.ts | 1 + .../src/singleton/apis/fetchAuthSession.ts | 6 +-- .../core/src/utils/deDupeAsyncFunction.ts | 38 ++++++++++++++ packages/core/src/utils/index.ts | 1 + .../src/sync/processors/subscription.ts | 2 +- 8 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 packages/core/__tests__/utils/deDupeAsyncRequests.test.ts create mode 100644 packages/core/src/utils/deDupeAsyncFunction.ts diff --git a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts index f74c8683fd6..2021b5a4c5b 100644 --- a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts +++ b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts @@ -6,6 +6,7 @@ import { AuthConfig } from '@aws-amplify/core'; import { assertTokenProviderConfig, decodeJWT, + deDupeAsyncFunction, } from '@aws-amplify/core/internals/utils'; import { initiateAuth } from '../utils/clients/CognitoIdentityProvider'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; @@ -13,7 +14,7 @@ import { assertAuthTokensWithRefreshToken } from '../utils/types'; import { AuthError } from '../../../errors/AuthError'; import { getUserContextData } from './userContextData'; -export const refreshAuthTokens: TokenRefresher = async ({ +const refreshAuthTokensFunction: TokenRefresher = async ({ tokens, authConfig, username, @@ -72,3 +73,5 @@ export const refreshAuthTokens: TokenRefresher = async ({ username, }; }; + +export const refreshAuthTokens = deDupeAsyncFunction(refreshAuthTokensFunction); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index e277dc5c209..c58d6eab0e2 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -300,13 +300,13 @@ "name": "[Analytics] record (Kinesis)", "path": "./dist/esm/analytics/kinesis/index.mjs", "import": "{ record }", - "limit": "44.47 kB" + "limit": "44.48 kB" }, { "name": "[Analytics] record (Kinesis Firehose)", "path": "./dist/esm/analytics/kinesis-firehose/index.mjs", "import": "{ record }", - "limit": "41.50 kB" + "limit": "41.54 kB" }, { "name": "[Analytics] record (Personalize)", @@ -384,7 +384,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "25.89 kB" + "limit": "25.94 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -396,19 +396,19 @@ "name": "[Auth] fetchMFAPreference (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ fetchMFAPreference }", - "limit": "8.30 kB" + "limit": "8.35 kB" }, { "name": "[Auth] verifyTOTPSetup (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ verifyTOTPSetup }", - "limit": "9.13 kB" + "limit": "9.18 kB" }, { "name": "[Auth] updatePassword (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updatePassword }", - "limit": "9.14 kB" + "limit": "9.19 kB" }, { "name": "[Auth] setUpTOTP (Cognito)", @@ -420,7 +420,7 @@ "name": "[Auth] updateUserAttributes (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updateUserAttributes }", - "limit": "8.41 kB" + "limit": "8.46 kB" }, { "name": "[Auth] getCurrentUser (Cognito)", @@ -432,7 +432,7 @@ "name": "[Auth] confirmUserAttribute (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmUserAttribute }", - "limit": "9.15 kB" + "limit": "9.19 kB" }, { "name": "[Auth] signInWithRedirect (Cognito)", diff --git a/packages/core/__tests__/utils/deDupeAsyncRequests.test.ts b/packages/core/__tests__/utils/deDupeAsyncRequests.test.ts new file mode 100644 index 00000000000..a017a439736 --- /dev/null +++ b/packages/core/__tests__/utils/deDupeAsyncRequests.test.ts @@ -0,0 +1,50 @@ +import { deDupeAsyncFunction } from '../../src/utils/deDupeAsyncFunction'; + +describe('dedupeAsyncFunction()', () => { + const numberOfConcurrentCalls = 10; + const mockServiceFunction = jest.fn(); + const mockReturnValue = { id: 1 }; + + beforeEach(() => { + mockServiceFunction.mockImplementation(async () => mockReturnValue); + }); + afterEach(() => { + mockServiceFunction.mockClear(); + }); + + it('should invoke the mockServiceFunction', async () => { + const deDupedFunction = deDupeAsyncFunction(mockServiceFunction); + + deDupedFunction(); + expect(mockServiceFunction).toHaveBeenCalledTimes(1); + }); + + it('should invoke the mockServiceFunction one time during concurrent sync calls', () => { + const deDupedFunction = deDupeAsyncFunction(mockServiceFunction); + for (let i = 0; i < numberOfConcurrentCalls; i++) { + deDupedFunction(); + } + expect(mockServiceFunction).toHaveBeenCalledTimes(1); + }); + + it('should return a value once the mockServiceFunction is resolved', async () => { + const deDupedFunction = deDupeAsyncFunction(mockServiceFunction); + expect(await deDupedFunction()).toEqual(mockReturnValue); + expect(mockServiceFunction).toHaveBeenCalledTimes(1); + }); + + it('should allow to invoke the mockServiceFunction again after the promise has being resolved', async () => { + const deDupedFunction = deDupeAsyncFunction(mockServiceFunction); + for (let i = 0; i < numberOfConcurrentCalls; i++) { + expect(deDupedFunction()).toBeInstanceOf(Promise); + } + + // resolves the promise + expect(await deDupedFunction()).toEqual(mockReturnValue); + + // should allow to call the mockServiceFunction again + deDupedFunction(); + + expect(mockServiceFunction).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 2c04e24d300..6d8a552ed3c 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -17,6 +17,7 @@ export { retry, urlSafeDecode, urlSafeEncode, + deDupeAsyncFunction, } from './utils'; export { parseAWSExports } from './parseAWSExports'; export { LegacyConfig } from './singleton/types'; diff --git a/packages/core/src/singleton/apis/fetchAuthSession.ts b/packages/core/src/singleton/apis/fetchAuthSession.ts index 63629cabe92..ae4c6885024 100644 --- a/packages/core/src/singleton/apis/fetchAuthSession.ts +++ b/packages/core/src/singleton/apis/fetchAuthSession.ts @@ -3,10 +3,8 @@ import { fetchAuthSession as fetchAuthSessionInternal } from './internal/fetchAuthSession'; import { Amplify } from '../Amplify'; -import { AuthSession, FetchAuthSessionOptions } from '../Auth/types'; +import { FetchAuthSessionOptions } from '../Auth/types'; -export const fetchAuthSession = ( - options?: FetchAuthSessionOptions -): Promise => { +export const fetchAuthSession = (options?: FetchAuthSessionOptions) => { return fetchAuthSessionInternal(Amplify, options); }; diff --git a/packages/core/src/utils/deDupeAsyncFunction.ts b/packages/core/src/utils/deDupeAsyncFunction.ts new file mode 100644 index 00000000000..5cf2d672ec7 --- /dev/null +++ b/packages/core/src/utils/deDupeAsyncFunction.ts @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// this will make the tsc-compliance-test to pass +type Awaited = T extends null | undefined + ? T // special case for `null | undefined` when not in `--strictNullChecks` mode + : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped + ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument + ? Awaited // recursively unwrap the value + : never // the argument to `then` was not callable + : T; // +/** + * returns in-flight promise if there is one + * + * @param asyncFunction - asyncFunction to be deduped. + * @returns - the return type of the callback + */ +export const deDupeAsyncFunction = ( + asyncFunction: (...args: A) => Promise +) => { + let inflightPromise: Promise> | undefined; + return async (...args: A): Promise> => { + if (inflightPromise) return inflightPromise; + + inflightPromise = new Promise(async (resolve, reject) => { + try { + const result = await asyncFunction(...args); + resolve(result); + } catch (error) { + reject(error); + } finally { + inflightPromise = undefined; + } + }); + + return inflightPromise; + }; +}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index c49c1d2e0af..9b713d2066e 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -15,3 +15,4 @@ export { export { urlSafeDecode } from './urlSafeDecode'; export { urlSafeEncode } from './urlSafeEncode'; export { deepFreeze } from './deepFreeze'; +export { deDupeAsyncFunction } from './deDupeAsyncFunction'; diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index 36ddaeb8513..6561d178717 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -279,7 +279,7 @@ class SubscriptionProcessor { try { // retrieving current token info from Cognito UserPools - const session = await await fetchAuthSession(); + const session = await fetchAuthSession(); oidcTokenPayload = session.tokens?.idToken?.payload; } catch (err) { // best effort to get jwt from Cognito