mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: implement ttl cache
This commit is contained in:
parent
1256711bcc
commit
e0fad2dccd
26 changed files with 202 additions and 54 deletions
|
@ -47,7 +47,7 @@
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@logto/shared": "workspace:*",
|
"@logto/shared": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@logto/shared": "workspace:*",
|
"@logto/shared": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@withtyped/postgres": "^0.8.1",
|
"@withtyped/postgres": "^0.8.1",
|
||||||
"@withtyped/server": "^0.8.1",
|
"@withtyped/server": "^0.8.1",
|
||||||
"accepts": "^1.3.8",
|
"accepts": "^1.3.8",
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
"@parcel/transformer-svg-react": "2.8.3",
|
"@parcel/transformer-svg-react": "2.8.3",
|
||||||
"@silverhand/eslint-config": "2.0.1",
|
"@silverhand/eslint-config": "2.0.1",
|
||||||
"@silverhand/eslint-config-react": "2.0.1",
|
"@silverhand/eslint-config-react": "2.0.1",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@silverhand/ts-config": "2.0.3",
|
"@silverhand/ts-config": "2.0.3",
|
||||||
"@silverhand/ts-config-react": "2.0.3",
|
"@silverhand/ts-config-react": "2.0.3",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
"@logto/phrases-ui": "workspace:*",
|
"@logto/phrases-ui": "workspace:*",
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@logto/shared": "workspace:*",
|
"@logto/shared": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"aws-sdk": "^2.1329.0",
|
"aws-sdk": "^2.1329.0",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"clean-deep": "^3.4.0",
|
"clean-deep": "^3.4.0",
|
||||||
|
@ -51,7 +51,6 @@
|
||||||
"iconv-lite": "0.6.3",
|
"iconv-lite": "0.6.3",
|
||||||
"jose": "^4.11.0",
|
"jose": "^4.11.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"keyv": "^4.5.2",
|
|
||||||
"koa": "^2.13.1",
|
"koa": "^2.13.1",
|
||||||
"koa-body": "^5.0.0",
|
"koa-body": "^5.0.0",
|
||||||
"koa-compose": "^4.1.0",
|
"koa-compose": "^4.1.0",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Keyv from 'keyv';
|
import { TtlCache } from '@logto/shared';
|
||||||
import type { AnyAsyncFunction } from 'p-memoize';
|
import type { AnyAsyncFunction } from 'p-memoize';
|
||||||
import pMemoize from 'p-memoize';
|
import pMemoize from 'p-memoize';
|
||||||
|
|
||||||
|
@ -10,14 +10,14 @@ export type WellKnownCacheKey = (typeof cacheKeys)[number];
|
||||||
const buildKey = (tenantId: string, key: WellKnownCacheKey) => `${tenantId}:${key}` as const;
|
const buildKey = (tenantId: string, key: WellKnownCacheKey) => `${tenantId}:${key}` as const;
|
||||||
|
|
||||||
class WellKnownCache {
|
class WellKnownCache {
|
||||||
// Not sure if we need guard value for `.has()` and `.get()`,
|
#cache = new TtlCache<string, unknown>(180_000 /* 3 minutes */);
|
||||||
// trust cache value for now.
|
|
||||||
#keyv = new Keyv({ ttl: 180_000 /* 3 minutes */ });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use for centralized well-known data caching.
|
* Use for centralized well-known data caching.
|
||||||
*
|
*
|
||||||
* WARN: You should store only well-known (public) data since it's a central cache.
|
* WARN:
|
||||||
|
* - You should store only well-known (public) data since it's a central cache.
|
||||||
|
* - The cache does not guard types.
|
||||||
*/
|
*/
|
||||||
use<FunctionToMemoize extends AnyAsyncFunction>(
|
use<FunctionToMemoize extends AnyAsyncFunction>(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
|
@ -26,20 +26,24 @@ class WellKnownCache {
|
||||||
) {
|
) {
|
||||||
return pMemoize(run, {
|
return pMemoize(run, {
|
||||||
cacheKey: () => buildKey(tenantId, key),
|
cacheKey: () => buildKey(tenantId, key),
|
||||||
cache: this.#keyv,
|
// Trust cache value type
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
cache: this.#cache as TtlCache<string, Awaited<ReturnType<FunctionToMemoize>>>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async invalidate(tenantId: string, keys: readonly WellKnownCacheKey[]) {
|
invalidate(tenantId: string, keys: readonly WellKnownCacheKey[]) {
|
||||||
return this.#keyv.delete(keys.map((key) => buildKey(tenantId, key)));
|
for (const key of keys) {
|
||||||
|
this.#cache.delete(buildKey(tenantId, key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async invalidateAll(tenantId: string) {
|
invalidateAll(tenantId: string) {
|
||||||
return this.invalidate(tenantId, cacheKeys);
|
this.invalidate(tenantId, cacheKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(tenantId: string, key: WellKnownCacheKey, value: unknown) {
|
set(tenantId: string, key: WellKnownCacheKey, value: unknown) {
|
||||||
return this.#keyv.set(buildKey(tenantId, key), value);
|
this.#cache.set(buildKey(tenantId, key), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ const { getPhrases } = createPhraseLibrary(
|
||||||
tenantId
|
tenantId
|
||||||
);
|
);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
await wellKnownCache.invalidateAll(tenantId);
|
wellKnownCache.invalidateAll(tenantId);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default function initOidc(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
interactions: {
|
interactions: {
|
||||||
url: async (ctx, interaction) => {
|
url: (ctx, interaction) => {
|
||||||
const isDemoApp = interaction.params.client_id === demoAppApplicationId;
|
const isDemoApp = interaction.params.client_id === demoAppApplicationId;
|
||||||
|
|
||||||
const appendParameters = (path: string) => {
|
const appendParameters = (path: string) => {
|
||||||
|
@ -158,7 +158,7 @@ export default function initOidc(
|
||||||
case 'login': {
|
case 'login': {
|
||||||
// Always fetch the latest sign-in experience config for demo app (live preview)
|
// Always fetch the latest sign-in experience config for demo app (live preview)
|
||||||
if (isDemoApp) {
|
if (isDemoApp) {
|
||||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSignUp =
|
const isSignUp =
|
||||||
|
|
|
@ -195,7 +195,7 @@ export default async function submitInteraction(
|
||||||
|
|
||||||
// Normally we don't need to manually invalidate TTL cache.
|
// Normally we don't need to manually invalidate TTL cache.
|
||||||
// This is for better OSS onboarding experience.
|
// This is for better OSS onboarding experience.
|
||||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||||
}
|
}
|
||||||
|
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function koaInteractionSie<StateT, ContextT, ResponseT>(
|
||||||
): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
if (noCache(ctx.headers)) {
|
if (noCache(ctx.headers)) {
|
||||||
await wellKnownCache.invalidate(tenantId, ['sie']);
|
wellKnownCache.invalidate(tenantId, ['sie']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signInExperience = await getSignInExperience();
|
const signInExperience = await getSignInExperience();
|
||||||
|
|
|
@ -41,8 +41,8 @@ const phraseRequest = createRequester({
|
||||||
tenantContext,
|
tenantContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
wellKnownCache.invalidateAll(tenantContext.id);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -59,8 +59,8 @@ const phraseRequest = createRequester({
|
||||||
tenantContext,
|
tenantContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
wellKnownCache.invalidateAll(tenantContext.id);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,8 @@ const tenantContext = new MockTenant(
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('GET /.well-known/sign-in-exp', () => {
|
describe('GET /.well-known/sign-in-exp', () => {
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
wellKnownCache.invalidateAll(tenantContext.id);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
koaGuard({ response: guardFullSignInExperience, status: 200 }),
|
koaGuard({ response: guardFullSignInExperience, status: 200 }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
if (noCache(ctx.headers)) {
|
if (noCache(ctx.headers)) {
|
||||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = await getFullSignInExperience();
|
ctx.body = await getFullSignInExperience();
|
||||||
|
@ -60,7 +60,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
if (noCache(ctx.headers)) {
|
if (noCache(ctx.headers)) {
|
||||||
await wellKnownCache.invalidate(tenantId, ['sie', 'phrases-lng-tags', 'phrases']);
|
wellKnownCache.invalidate(tenantId, ['sie', 'phrases-lng-tags', 'phrases']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@peculiar/webcrypto": "^1.3.3",
|
"@peculiar/webcrypto": "^1.3.3",
|
||||||
"@silverhand/eslint-config": "2.0.1",
|
"@silverhand/eslint-config": "2.0.1",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@silverhand/ts-config": "2.0.3",
|
"@silverhand/ts-config": "2.0.3",
|
||||||
"@types/expect-puppeteer": "^5.0.3",
|
"@types/expect-puppeteer": "^5.0.3",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
import type { SignInExperience } from '@logto/schemas';
|
||||||
|
|
||||||
import { adminTenantApi } from '#src/api/api.js';
|
import { adminTenantApi, authedAdminApi } from '#src/api/api.js';
|
||||||
import { api } from '#src/api/index.js';
|
import { api } from '#src/api/index.js';
|
||||||
|
import { generateUserId } from '#src/utils.js';
|
||||||
|
|
||||||
describe('.well-known api', () => {
|
describe('.well-known api', () => {
|
||||||
it('get /.well-known/sign-in-exp for console', async () => {
|
it('get /.well-known/sign-in-exp for console', async () => {
|
||||||
|
@ -33,4 +34,19 @@ describe('.well-known api', () => {
|
||||||
// Should support sign-in and register
|
// Should support sign-in and register
|
||||||
expect(response).toMatchObject({ signInMode: 'SignInAndRegister' });
|
expect(response).toMatchObject({ signInMode: 'SignInAndRegister' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use cached version if no-cache header is not present', async () => {
|
||||||
|
const response1 = await api.get('.well-known/sign-in-exp').json<SignInExperience>();
|
||||||
|
|
||||||
|
const randomId = generateUserId();
|
||||||
|
const customContent = { foo: randomId };
|
||||||
|
await authedAdminApi.patch('sign-in-exp', { json: { customContent } }).json<SignInExperience>();
|
||||||
|
|
||||||
|
const response2 = await api
|
||||||
|
.get('.well-known/sign-in-exp', { headers: { 'cache-control': '' } })
|
||||||
|
.json<SignInExperience>();
|
||||||
|
|
||||||
|
expect(response2.customContent.foo).not.toBe(randomId);
|
||||||
|
expect(response2).toStrictEqual(response1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/language-kit": "workspace:*",
|
"@logto/language-kit": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/language-kit": "workspace:*",
|
"@logto/language-kit": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@silverhand/eslint-config": "2.0.1",
|
"@silverhand/eslint-config": "2.0.1",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@silverhand/ts-config": "2.0.3",
|
"@silverhand/ts-config": "2.0.3",
|
||||||
"@types/inquirer": "^9.0.0",
|
"@types/inquirer": "^9.0.0",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"find-up": "^6.3.0",
|
"find-up": "^6.3.0",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './function.js';
|
export * from './function.js';
|
||||||
export * from './object.js';
|
export * from './object.js';
|
||||||
export { default as findPackage } from './find-package.js';
|
export { default as findPackage } from './find-package.js';
|
||||||
|
export * from './ttl-cache.js';
|
||||||
|
|
84
packages/shared/src/utils/ttl-cache.test.ts
Normal file
84
packages/shared/src/utils/ttl-cache.test.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { TtlCache } from './ttl-cache.js';
|
||||||
|
|
||||||
|
const { jest } = import.meta;
|
||||||
|
|
||||||
|
describe('TtlCache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached value after a long time if ttl is not set', () => {
|
||||||
|
jest.setSystemTime(0);
|
||||||
|
|
||||||
|
const cache = new TtlCache();
|
||||||
|
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||||
|
|
||||||
|
cache.set('foo', someObject);
|
||||||
|
|
||||||
|
jest.setSystemTime(100_000_000);
|
||||||
|
expect(cache.get('foo')).toBe(someObject);
|
||||||
|
expect(cache.has('foo')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached value and honor ttl', () => {
|
||||||
|
jest.setSystemTime(0);
|
||||||
|
|
||||||
|
const cache = new TtlCache(100);
|
||||||
|
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||||
|
|
||||||
|
cache.set(123, someObject);
|
||||||
|
cache.set('foo', someObject, 99);
|
||||||
|
|
||||||
|
jest.setSystemTime(100);
|
||||||
|
expect(cache.get(123)).toBe(someObject);
|
||||||
|
expect(cache.has(123)).toBe(true);
|
||||||
|
expect(cache.get('123')).toBe(undefined);
|
||||||
|
expect(cache.has('123')).toBe(false);
|
||||||
|
expect(cache.get('foo')).toBe(undefined);
|
||||||
|
expect(cache.has('foo')).toBe(false);
|
||||||
|
|
||||||
|
jest.setSystemTime(101);
|
||||||
|
expect(cache.get(123)).toBe(undefined);
|
||||||
|
expect(cache.has(123)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to delete value before ttl', () => {
|
||||||
|
const cache = new TtlCache(100);
|
||||||
|
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||||
|
|
||||||
|
cache.set('foo', someObject);
|
||||||
|
cache.delete('foo');
|
||||||
|
cache.delete('bar');
|
||||||
|
|
||||||
|
expect(cache.get('foo')).toBe(undefined);
|
||||||
|
expect(cache.has('foo')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to clear all values', () => {
|
||||||
|
const cache = new TtlCache(100);
|
||||||
|
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||||
|
|
||||||
|
cache.set('foo', someObject);
|
||||||
|
cache.set('bar', someObject);
|
||||||
|
cache.set(123, 456);
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
expect(cache.get('foo')).toBe(undefined);
|
||||||
|
expect(cache.has('foo')).toBe(false);
|
||||||
|
expect(cache.get('bar')).toBe(undefined);
|
||||||
|
expect(cache.has('bar')).toBe(false);
|
||||||
|
expect(cache.get(123)).toBe(undefined);
|
||||||
|
expect(cache.has(123)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw undefined when value is undefined', () => {
|
||||||
|
const cache = new TtlCache();
|
||||||
|
expect(() => {
|
||||||
|
cache.set(1, undefined);
|
||||||
|
}).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
});
|
46
packages/shared/src/utils/ttl-cache.ts
Normal file
46
packages/shared/src/utils/ttl-cache.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
export class TtlCache<Key, Value> {
|
||||||
|
data = new Map<Key, Value>();
|
||||||
|
expiration = new Map<Key, number>();
|
||||||
|
|
||||||
|
constructor(public readonly ttl = Number.POSITIVE_INFINITY) {}
|
||||||
|
|
||||||
|
#purge(key: Key) {
|
||||||
|
const expiration = this.expiration.get(key);
|
||||||
|
|
||||||
|
if (expiration !== undefined && expiration < Date.now()) {
|
||||||
|
this.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: Key, value: Value, ttl = this.ttl) {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new TypeError('Value cannot be undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expiration.set(key, Date.now() + ttl);
|
||||||
|
this.data.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: Key): Value | undefined {
|
||||||
|
this.#purge(key);
|
||||||
|
|
||||||
|
return this.data.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: Key) {
|
||||||
|
this.#purge(key);
|
||||||
|
|
||||||
|
return this.data.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: Key) {
|
||||||
|
this.expiration.delete(key);
|
||||||
|
|
||||||
|
return this.data.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.expiration.clear();
|
||||||
|
this.data.clear();
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/language-kit": "workspace:*",
|
"@logto/language-kit": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.4.1"
|
"@silverhand/essentials": "2.4.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"@jest/types": "^29.0.3",
|
"@jest/types": "^29.0.3",
|
||||||
"@silverhand/eslint-config": "2.0.1",
|
"@silverhand/eslint-config": "2.0.1",
|
||||||
"@silverhand/eslint-config-react": "2.0.1",
|
"@silverhand/eslint-config-react": "2.0.1",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@silverhand/ts-config": "2.0.3",
|
"@silverhand/ts-config": "2.0.3",
|
||||||
"@types/color": "^3.0.3",
|
"@types/color": "^3.0.3",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"@react-spring/web": "^9.6.1",
|
"@react-spring/web": "^9.6.1",
|
||||||
"@silverhand/eslint-config": "2.0.1",
|
"@silverhand/eslint-config": "2.0.1",
|
||||||
"@silverhand/eslint-config-react": "2.0.1",
|
"@silverhand/eslint-config-react": "2.0.1",
|
||||||
"@silverhand/essentials": "^2.4.1",
|
"@silverhand/essentials": "2.4.1",
|
||||||
"@silverhand/jest-config": "1.2.2",
|
"@silverhand/jest-config": "1.2.2",
|
||||||
"@silverhand/ts-config": "2.0.3",
|
"@silverhand/ts-config": "2.0.3",
|
||||||
"@silverhand/ts-config-react": "2.0.3",
|
"@silverhand/ts-config-react": "2.0.3",
|
||||||
|
|
|
@ -32,7 +32,7 @@ importers:
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@logto/shared': workspace:*
|
'@logto/shared': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/inquirer': ^9.0.0
|
'@types/inquirer': ^9.0.0
|
||||||
'@types/jest': ^29.4.0
|
'@types/jest': ^29.4.0
|
||||||
|
@ -116,7 +116,7 @@ importers:
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@logto/shared': workspace:*
|
'@logto/shared': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/jest-config': ^2.0.1
|
'@silverhand/jest-config': ^2.0.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/accepts': ^1.3.5
|
'@types/accepts': ^1.3.5
|
||||||
|
@ -196,7 +196,7 @@ importers:
|
||||||
'@parcel/transformer-svg-react': 2.8.3
|
'@parcel/transformer-svg-react': 2.8.3
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/eslint-config-react': 2.0.1
|
'@silverhand/eslint-config-react': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@silverhand/ts-config-react': 2.0.3
|
'@silverhand/ts-config-react': 2.0.3
|
||||||
'@tsconfig/docusaurus': ^1.0.5
|
'@tsconfig/docusaurus': ^1.0.5
|
||||||
|
@ -346,7 +346,7 @@ importers:
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@logto/shared': workspace:*
|
'@logto/shared': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/debug': ^4.1.7
|
'@types/debug': ^4.1.7
|
||||||
'@types/etag': ^1.8.1
|
'@types/etag': ^1.8.1
|
||||||
|
@ -385,7 +385,6 @@ importers:
|
||||||
jest-matcher-specific-error: ^1.0.0
|
jest-matcher-specific-error: ^1.0.0
|
||||||
jose: ^4.11.0
|
jose: ^4.11.0
|
||||||
js-yaml: ^4.1.0
|
js-yaml: ^4.1.0
|
||||||
keyv: ^4.5.2
|
|
||||||
koa: ^2.13.1
|
koa: ^2.13.1
|
||||||
koa-body: ^5.0.0
|
koa-body: ^5.0.0
|
||||||
koa-compose: ^4.1.0
|
koa-compose: ^4.1.0
|
||||||
|
@ -444,7 +443,6 @@ importers:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
jose: 4.11.0
|
jose: 4.11.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
keyv: 4.5.2
|
|
||||||
koa: 2.13.4
|
koa: 2.13.4
|
||||||
koa-body: 5.0.0
|
koa-body: 5.0.0
|
||||||
koa-compose: 4.1.0
|
koa-compose: 4.1.0
|
||||||
|
@ -577,7 +575,7 @@ importers:
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@peculiar/webcrypto': ^1.3.3
|
'@peculiar/webcrypto': ^1.3.3
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/expect-puppeteer': ^5.0.3
|
'@types/expect-puppeteer': ^5.0.3
|
||||||
'@types/jest': ^29.4.0
|
'@types/jest': ^29.4.0
|
||||||
|
@ -629,7 +627,7 @@ importers:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@logto/language-kit': workspace:*
|
'@logto/language-kit': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
|
@ -652,7 +650,7 @@ importers:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@logto/language-kit': workspace:*
|
'@logto/language-kit': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
buffer: ^5.7.1
|
buffer: ^5.7.1
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
|
@ -681,7 +679,7 @@ importers:
|
||||||
'@logto/phrases': workspace:*
|
'@logto/phrases': workspace:*
|
||||||
'@logto/phrases-ui': workspace:*
|
'@logto/phrases-ui': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/inquirer': ^9.0.0
|
'@types/inquirer': ^9.0.0
|
||||||
'@types/jest': ^29.4.0
|
'@types/jest': ^29.4.0
|
||||||
|
@ -734,7 +732,7 @@ importers:
|
||||||
'@logto/core-kit': workspace:*
|
'@logto/core-kit': workspace:*
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/jest': ^29.4.0
|
'@types/jest': ^29.4.0
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
|
@ -771,7 +769,7 @@ importers:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@logto/language-kit': workspace:*
|
'@logto/language-kit': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
|
@ -801,7 +799,7 @@ importers:
|
||||||
'@logto/language-kit': workspace:*
|
'@logto/language-kit': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/eslint-config-react': 2.0.1
|
'@silverhand/eslint-config-react': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/color': ^3.0.3
|
'@types/color': ^3.0.3
|
||||||
'@types/jest': ^29.4.0
|
'@types/jest': ^29.4.0
|
||||||
|
@ -890,7 +888,7 @@ importers:
|
||||||
'@react-spring/web': ^9.6.1
|
'@react-spring/web': ^9.6.1
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/eslint-config-react': 2.0.1
|
'@silverhand/eslint-config-react': 2.0.1
|
||||||
'@silverhand/essentials': ^2.4.1
|
'@silverhand/essentials': 2.4.1
|
||||||
'@silverhand/jest-config': 1.2.2
|
'@silverhand/jest-config': 1.2.2
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@silverhand/ts-config-react': 2.0.3
|
'@silverhand/ts-config-react': 2.0.3
|
||||||
|
|
Loading…
Reference in a new issue