diff --git a/.changeset/spicy-pears-serve.md b/.changeset/spicy-pears-serve.md new file mode 100644 index 000000000..eb3c7b988 --- /dev/null +++ b/.changeset/spicy-pears-serve.md @@ -0,0 +1,5 @@ +--- +"@logto/core": minor +--- + +implement a central cache store to cache well-known with Redis implementation diff --git a/packages/core/package.json b/packages/core/package.json index 173a6839b..83a89ea60 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -68,7 +68,6 @@ "lru-cache": "^8.0.0", "nanoid": "^4.0.0", "oidc-provider": "^8.0.0", - "p-memoize": "^7.1.1", "p-retry": "^5.1.2", "pg-protocol": "^1.6.0", "redis": "^4.6.5", diff --git a/packages/core/src/caches/well-known.ts b/packages/core/src/caches/well-known.ts index 93e947a9c..321c15bea 100644 --- a/packages/core/src/caches/well-known.ts +++ b/packages/core/src/caches/well-known.ts @@ -17,12 +17,25 @@ const defaultCacheKey = '#'; export type WellKnownCacheType = keyof WellKnownMap; -type CacheKeyConfig = - | [Type] - | [Type, (...args: Args) => string]; +/** + * The array tuple to determine how cache will be built. + * + * - If only `Type` is given, the cache key should be resolved as `${valueof Type}:#`. + * - If both parameters are given, the cache key will be built dynamically by executing + * the second element (which is a function) by passing current calling arguments: + * `${valueof Type}:${valueof CacheKey(...args)}`. + * + * @template Args The function arguments for the cache key builder to resolve. + * @template Type The {@link WellKnownCacheType cache type}. + */ +type CacheKeyConfig< + Args extends unknown[], + Type = WellKnownCacheType, + CacheKey = (...args: Args) => string +> = [Type] | [Type, CacheKey]; // Cannot use generic type here, but direct type works. -// See https://github.com/microsoft/TypeScript/issues/27808#issuecomment-1207161877 +// See [this issue](https://github.com/microsoft/TypeScript/issues/27808#issuecomment-1207161877) for details. // WARN: You should carefully check key and return type mapping since the implementation signature doesn't do this. function getValueGuard(type: Type): ZodType; @@ -46,9 +59,27 @@ function getValueGuard(type: WellKnownCacheType): ZodType( type: Type, key: string @@ -58,6 +89,10 @@ export class WellKnownCache { return trySafe(() => getValueGuard(type).parse(JSON.parse(data ?? ''))); } + /** + * Set value to the inner cache store for the given type and key. + * The given value will be stringify without format validation before storing into the cache. + */ async set( type: Type, key: string, @@ -66,10 +101,18 @@ export class WellKnownCache { return this.cacheStore.set(this.cacheKey(type, key), JSON.stringify(value)); } + /** Delete value from the inner cache store for the given type and key. */ async delete(type: WellKnownCacheType, key: string) { return this.cacheStore.delete(this.cacheKey(type, key)); } + /** + * Create a wrapper of the given function, which invalidates a set of keys in cache + * after the function runs successfully. + * + * @param run The function to wrap. + * @param types An array of {@link CacheKeyConfig}. + */ mutate( run: (...args: Args) => Promise, ...types: Array> @@ -95,6 +138,13 @@ export class WellKnownCache { return mutated; } + /** + * [Memoize](https://en.wikipedia.org/wiki/Memoization) a function and cache the result. The function execution + * will be also cached, which means there will be only one execution at a time. + * + * @param run The function to memoize. + * @param config The object to determine how cache key will be built. See {@link CacheKeyConfig} for details. + */ memoize( run: (...args: Args) => Promise>, [type, cacheKey]: CacheKeyConfig diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index d68b99004..42a8e59bd 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -53,11 +53,9 @@ export const createSignInExperienceLibrary = ( }); }; - const getSignInExperience = findDefaultSignInExperience; - const getFullSignInExperience = async (): Promise => { const [signInExperience, logtoConnectors] = await Promise.all([ - getSignInExperience(), + findDefaultSignInExperience(), getLogtoConnectors(), ]); @@ -89,7 +87,6 @@ export const createSignInExperienceLibrary = ( return { validateLanguageInfo, removeUnavailableSocialConnectorTargets, - getSignInExperience, getFullSignInExperience, }; }; diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index 3aec98a74..0f7481cbc 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -99,7 +99,11 @@ const baseProviderMock = { const tenantContext = new MockTenant( createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), - undefined, + { + signInExperiences: { + findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), + }, + }, { getLogtoConnectorById: async (connectorId: string) => { const connector = await getLogtoConnectorByIdHelper(connectorId); @@ -114,11 +118,6 @@ const tenantContext = new MockTenant( // @ts-expect-error return connector as LogtoConnector; }, - }, - { - signInExperiences: { - getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), - }, } ); diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index 055dba1be..d07b5b62a 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -50,7 +50,7 @@ export type RouterContext = T extends Router ? Contex export default function interactionRoutes( ...[anonymousRouter, tenant]: RouterInitArgs ) { - const { provider, queries, libraries, id: tenantId } = tenant; + const { provider, queries, libraries } = tenant; const router = // @ts-expect-error for good koa types // eslint-disable-next-line no-restricted-syntax @@ -69,7 +69,7 @@ export default function interactionRoutes( profile: profileGuard.optional(), }), }), - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), async (ctx, next) => { const { event, identifier, profile } = ctx.guard.body; const { signInExperience, createLog } = ctx; @@ -119,7 +119,7 @@ export default function interactionRoutes( router.put( `${interactionPrefix}/event`, koaGuard({ body: z.object({ event: eventGuard }) }), - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), async (ctx, next) => { const { event } = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -157,7 +157,7 @@ export default function interactionRoutes( koaGuard({ body: identifierPayloadGuard, }), - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), async (ctx, next) => { const identifierPayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -194,7 +194,7 @@ export default function interactionRoutes( koaGuard({ body: profileGuard, }), - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), async (ctx, next) => { const profilePayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -231,7 +231,7 @@ export default function interactionRoutes( koaGuard({ body: profileGuard, }), - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), async (ctx, next) => { const profilePayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -284,7 +284,7 @@ export default function interactionRoutes( // Submit Interaction router.post( `${interactionPrefix}/submit`, - koaInteractionSie(libraries.signInExperiences), + koaInteractionSie(queries), koaInteractionHooks(tenant), async (ctx, next) => { const { interactionDetails, createLog } = ctx; diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts index 8cd0afa2e..bc66794aa 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts @@ -1,7 +1,7 @@ import type { SignInExperience } from '@logto/schemas'; import type { MiddlewareType } from 'koa'; -import type { SignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js'; +import type Queries from '#src/tenants/Queries.js'; import type { WithInteractionDetailsContext } from './koa-interaction-details.js'; @@ -10,14 +10,10 @@ export type WithInteractionSieContext = WithInteractionDetailsContext< }; export default function koaInteractionSie({ - getSignInExperience, -}: SignInExperienceLibrary): MiddlewareType< - StateT, - WithInteractionSieContext, - ResponseT -> { + signInExperiences: { findDefaultSignInExperience }, +}: Queries): MiddlewareType, ResponseT> { return async (ctx, next) => { - const signInExperience = await getSignInExperience(); + const signInExperience = await findDefaultSignInExperience(); ctx.signInExperience = signInExperience; diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 3f812f9f8..539e149ca 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -15,10 +15,13 @@ export default function wellKnownRoutes( ...[router, { libraries, queries, id: tenantId }]: RouterInitArgs ) { const { - signInExperiences: { getSignInExperience, getFullSignInExperience }, + signInExperiences: { getFullSignInExperience }, phrases: { getPhrases }, } = libraries; - const { findAllCustomLanguageTags } = queries.customPhrases; + const { + customPhrases: { findAllCustomLanguageTags }, + signInExperiences: { findDefaultSignInExperience }, + } = queries; if (tenantId === adminTenantId) { router.get('/.well-known/endpoints/:tenantId', async (ctx, next) => { @@ -60,7 +63,7 @@ export default function wellKnownRoutes( const { languageInfo: { autoDetect, fallbackLanguage }, - } = await getSignInExperience(); + } = await findDefaultSignInExperience(); const acceptableLanguages = conditionalArray( lng, diff --git a/packages/integration-tests/src/api/api.ts b/packages/integration-tests/src/api/api.ts index be5b8d5d3..d9ba4fcc6 100644 --- a/packages/integration-tests/src/api/api.ts +++ b/packages/integration-tests/src/api/api.ts @@ -4,7 +4,6 @@ import { logtoConsoleUrl, logtoUrl } from '#src/constants.js'; const api = got.extend({ prefixUrl: new URL('/api', logtoUrl), - headers: { 'cache-control': 'no-cache' }, }); export default api; @@ -18,7 +17,6 @@ export const authedAdminApi = api.extend({ export const adminTenantApi = got.extend({ prefixUrl: new URL('/api', logtoConsoleUrl), - headers: { 'cache-control': 'no-cache' }, }); export const authedAdminTenantApi = adminTenantApi.extend({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94b677801..efe18de7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3131,9 +3131,6 @@ importers: oidc-provider: specifier: ^8.0.0 version: 8.0.0 - p-memoize: - specifier: ^7.1.1 - version: 7.1.1 p-retry: specifier: ^5.1.2 version: 5.1.2 @@ -14867,6 +14864,7 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + dev: true /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -15492,14 +15490,6 @@ packages: aggregate-error: 3.1.0 dev: true - /p-memoize@7.1.1: - resolution: {integrity: sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA==} - engines: {node: '>=14.16'} - dependencies: - mimic-fn: 4.0.0 - type-fest: 3.5.2 - dev: false - /p-queue@7.3.4: resolution: {integrity: sha512-esox8CWt0j9EZECFvkFl2WNPat8LN4t7WWeXq73D9ha0V96qPRufApZi4ZhPwXAln1uVVal429HVVKPa2X0yQg==} engines: {node: '>=12'}