0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(core): add cache class and invalidate cache after onboarding

This commit is contained in:
Gao Sun 2023-03-15 23:40:04 +08:00
parent dd91ebddfa
commit ae389c0a7a
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
9 changed files with 62 additions and 37 deletions

View file

@ -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<FunctionToMemoize extends AnyAsyncFunction>(
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 = <FunctionToMemoize extends AnyAsyncFunction>(
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();

View file

@ -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();
});

View file

@ -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
);

View file

@ -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<FullSignInExperience> => {
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,

View file

@ -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,
};
};

View file

@ -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 } });

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();
});