From ae389c0a7a446d7b7809c62320ec6b926cded31f Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 15 Mar 2023 23:40:04 +0800 Subject: [PATCH] refactor(core): add cache class and invalidate cache after onboarding --- packages/core/src/caches/well-known.ts | 54 +++++++++++++------ packages/core/src/libraries/phrase.test.ts | 4 +- packages/core/src/libraries/phrase.ts | 8 +-- .../src/libraries/sign-in-experience/index.ts | 10 ++-- .../core/src/queries/sign-in-experience.ts | 4 -- .../interaction/actions/submit-interaction.ts | 7 ++- ...ell-known.phrases.content-language.test.ts | 4 +- .../src/routes/well-known.phrases.test.ts | 4 +- packages/core/src/routes/well-known.test.ts | 4 +- 9 files changed, 62 insertions(+), 37 deletions(-) diff --git a/packages/core/src/caches/well-known.ts b/packages/core/src/caches/well-known.ts index 3e9901b37..f8fd5f1b8 100644 --- a/packages/core/src/caches/well-known.ts +++ b/packages/core/src/caches/well-known.ts @@ -2,29 +2,49 @@ import Keyv from 'keyv'; import type { AnyAsyncFunction } from 'p-memoize'; import pMemoize from 'p-memoize'; -const cacheKeys = Object.freeze(['sie', 'sie-full', 'phrases', 'lng-tags'] as const); +const cacheKeys = Object.freeze(['sie', 'sie-full', 'phrases', 'phrases-lng-tags'] as const); /** Well-known data type key for cache. */ export type WellKnownCacheKey = (typeof cacheKeys)[number]; -// Not sure if we need guard value for `.has()` and `.get()`, -// trust cache value for now. -const wellKnownCache = new Keyv({ ttl: 300_000 /* 5 minutes */ }); +const buildKey = (tenantId: string, key: WellKnownCacheKey) => `${tenantId}:${key}` as const; + +class WellKnownCache { + // Not sure if we need guard value for `.has()` and `.get()`, + // trust cache value for now. + #keyv = new Keyv({ ttl: 180_000 /* 3 minutes */ }); + + /** + * Use for centralized well-known data caching. + * + * WARN: You should store only well-known (public) data since it's a central cache. + */ + use( + tenantId: string, + key: WellKnownCacheKey, + run: FunctionToMemoize + ) { + return pMemoize(run, { + cacheKey: () => buildKey(tenantId, key), + cache: this.#keyv, + }); + } + + async invalidate(tenantId: string, keys = [...cacheKeys]) { + return this.#keyv.delete(keys.map((key) => buildKey(tenantId, key))); + } + + async set(tenantId: string, key: WellKnownCacheKey, value: unknown) { + return this.#keyv.set(buildKey(tenantId, key), value); + } +} /** - * Use for centralized well-known data caching. + * The central TTL cache for well-known data. The default TTL is 3 minutes. + * + * This cache is intended for public APIs that are tolerant for data freshness. + * For Management APIs, you should use uncached functions instead. * * WARN: You should store only well-known (public) data since it's a central cache. */ -export const useWellKnownCache = ( - tenantId: string, - key: WellKnownCacheKey, - run: FunctionToMemoize -) => - pMemoize(run, { - cacheKey: () => `${tenantId}:${key}`, - cache: wellKnownCache, - }); - -export const invalidateWellKnownCache = async (tenantId: string) => - wellKnownCache.delete(cacheKeys.map((key) => `${tenantId}:${key}` as const)); +export const wellKnownCache = new WellKnownCache(); diff --git a/packages/core/src/libraries/phrase.test.ts b/packages/core/src/libraries/phrase.test.ts index 8ab9410dd..1d8d0e665 100644 --- a/packages/core/src/libraries/phrase.test.ts +++ b/packages/core/src/libraries/phrase.test.ts @@ -11,7 +11,7 @@ import { zhCnTag, zhHkTag, } from '#src/__mocks__/custom-phrase.js'; -import { invalidateWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; import RequestError from '#src/errors/RequestError/index.js'; import { MockQueries } from '#src/test-utils/tenant.js'; @@ -50,7 +50,7 @@ const { getPhrases } = createPhraseLibrary( ); afterEach(async () => { - await invalidateWellKnownCache(tenantId); + await wellKnownCache.invalidate(tenantId); jest.clearAllMocks(); }); diff --git a/packages/core/src/libraries/phrase.ts b/packages/core/src/libraries/phrase.ts index 05743c0ae..fdb47ac97 100644 --- a/packages/core/src/libraries/phrase.ts +++ b/packages/core/src/libraries/phrase.ts @@ -4,7 +4,7 @@ import type { CustomPhrase } from '@logto/schemas'; import cleanDeep from 'clean-deep'; import deepmerge from 'deepmerge'; -import { useWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; import type Queries from '#src/tenants/Queries.js'; export const createPhraseLibrary = (queries: Queries, tenantId: string) => { @@ -31,11 +31,11 @@ export const createPhraseLibrary = (queries: Queries, tenantId: string) => { ); }; - const getPhrases = useWellKnownCache(tenantId, 'phrases', _getPhrases); + const getPhrases = wellKnownCache.use(tenantId, 'phrases', _getPhrases); - const getAllCustomLanguageTags = useWellKnownCache( + const getAllCustomLanguageTags = wellKnownCache.use( tenantId, - 'lng-tags', + 'phrases-lng-tags', findAllCustomLanguageTags ); diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index d9f14ab54..3f1171c0e 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -5,7 +5,7 @@ import { SignInExperiences, ConnectorType } from '@logto/schemas'; import { deduplicate } from '@silverhand/essentials'; import { z } from 'zod'; -import { useWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; import RequestError from '#src/errors/RequestError/index.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; import type Queries from '#src/tenants/Queries.js'; @@ -55,7 +55,7 @@ export const createSignInExperienceLibrary = ( }); }; - const getSignInExperience = useWellKnownCache(tenantId, 'sie', findDefaultSignInExperience); + const getSignInExperience = wellKnownCache.use(tenantId, 'sie', findDefaultSignInExperience); const _getFullSignInExperience = async (): Promise => { const [signInExperience, logtoConnectors] = await Promise.all([ @@ -88,7 +88,11 @@ export const createSignInExperienceLibrary = ( }; }; - const getFullSignInExperience = useWellKnownCache(tenantId, 'sie-full', _getFullSignInExperience); + const getFullSignInExperience = wellKnownCache.use( + tenantId, + 'sie-full', + _getFullSignInExperience + ); return { validateLanguageInfo, diff --git a/packages/core/src/queries/sign-in-experience.ts b/packages/core/src/queries/sign-in-experience.ts index 1740342d9..6872660b1 100644 --- a/packages/core/src/queries/sign-in-experience.ts +++ b/packages/core/src/queries/sign-in-experience.ts @@ -18,10 +18,6 @@ export const createSignInExperienceQueries = (pool: CommonQueryMethods) => { return { updateDefaultSignInExperience, - /** - * NOTE: Use `getSignInExperience()` from sign-in experience library - * if possible since that function leverages cache. - */ findDefaultSignInExperience, }; }; diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 4b3bed01f..82e4171e9 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -10,6 +10,7 @@ import { } from '@logto/schemas'; import { conditional, conditionalArray } from '@silverhand/essentials'; +import { wellKnownCache } from '#src/caches/well-known.js'; import { EnvSet } from '#src/env-set/index.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; import { assignInteractionResults } from '#src/libraries/session.js'; @@ -149,7 +150,7 @@ const parseUserProfile = async ( export default async function submitInteraction( interaction: VerifiedInteractionResult, ctx: WithInteractionDetailsContext, - { provider, libraries, connectors, queries }: TenantContext, + { provider, libraries, connectors, queries, id: tenantId }: TenantContext, log?: LogEntry ) { const { hasActiveUsers, findUserById, updateUserById } = queries.users; @@ -191,6 +192,10 @@ export default async function submitInteraction( await updateDefaultSignInExperience({ signInMode: isCloud ? SignInMode.SignInAndRegister : SignInMode.SignIn, }); + + // Normally we don't need to manually invalidate TTL cache. + // This is for better OSS onboarding experience. + await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']); } await assignInteractionResults(ctx, provider, { login: { accountId: id } }); diff --git a/packages/core/src/routes/well-known.phrases.content-language.test.ts b/packages/core/src/routes/well-known.phrases.content-language.test.ts index 2e5282c18..2134cde7b 100644 --- a/packages/core/src/routes/well-known.phrases.content-language.test.ts +++ b/packages/core/src/routes/well-known.phrases.content-language.test.ts @@ -4,7 +4,7 @@ import { pickDefault } from '@logto/shared/esm'; import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; -import { invalidateWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -42,7 +42,7 @@ const phraseRequest = createRequester({ }); afterEach(async () => { - await invalidateWellKnownCache(tenantContext.id); + await wellKnownCache.invalidate(tenantContext.id); jest.clearAllMocks(); }); diff --git a/packages/core/src/routes/well-known.phrases.test.ts b/packages/core/src/routes/well-known.phrases.test.ts index 98cfa491d..87d6a9ac3 100644 --- a/packages/core/src/routes/well-known.phrases.test.ts +++ b/packages/core/src/routes/well-known.phrases.test.ts @@ -4,7 +4,7 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { zhCnTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; -import { invalidateWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; import Queries from '#src/tenants/Queries.js'; import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { MockTenant } from '#src/test-utils/tenant.js'; @@ -60,7 +60,7 @@ const phraseRequest = createRequester({ }); afterEach(async () => { - await invalidateWellKnownCache(tenantContext.id); + await wellKnownCache.invalidate(tenantContext.id); jest.clearAllMocks(); }); diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 1258dd477..c05e87b1a 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -10,7 +10,7 @@ import { mockWechatConnector, mockWechatNativeConnector, } from '#src/__mocks__/index.js'; -import { invalidateWellKnownCache } from '#src/caches/well-known.js'; +import { wellKnownCache } from '#src/caches/well-known.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); @@ -56,7 +56,7 @@ const tenantContext = new MockTenant( describe('GET /.well-known/sign-in-exp', () => { afterEach(async () => { - await invalidateWellKnownCache(tenantContext.id); + await wellKnownCache.invalidate(tenantContext.id); jest.clearAllMocks(); });