Skip to content

Commit 2698e85

Browse files
fix: rework and extract cache utils (#3590)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 3e7f7f5 commit 2698e85

7 files changed

Lines changed: 899 additions & 118 deletions

File tree

build.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default defineBuildConfig({
99
'@rspack/core',
1010
'oxc-parser',
1111
'@babel/parser',
12+
'unstorage',
1213
'unplugin-vue-router',
1314
'unplugin-vue-router/options'
1415
]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"ufo": "^1.6.1",
109109
"unplugin": "^2.3.2",
110110
"unplugin-vue-router": "^0.12.0",
111+
"unstorage": "^1.16.0",
111112
"vue-i18n": "^11.1.3",
112113
"vue-router": "^4.5.0"
113114
},

pnpm-lock.yaml

Lines changed: 771 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/server/plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { defineNitroPlugin } from 'nitropack/runtime'
66
import { tryUseI18nContext, createI18nContext } from './context'
77
import { createDefaultLocaleDetector, createUserLocaleDetector } from './utils/locale-detector'
88
import { pickNested } from './utils/messages-utils'
9-
import { getAllMergedMessages, getMergedMessages, isLocaleWithFallbacksCacheable } from './utils/messages'
9+
import { isLocaleWithFallbacksCacheable } from './utils/cache'
10+
import { getAllMergedMessages, getMergedMessages } from './utils/messages'
1011
import { getFallbackLocaleCodes } from '../shared/messages'
1112
// @ts-expect-error virtual file
1213
import { appId } from '#internal/nuxt.config.mjs'

src/runtime/server/routes/messages.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { deepCopy } from '@intlify/shared'
22
import { defineCachedEventHandler } from 'nitropack/runtime'
3-
import { getRouterParam, createError, H3Event, defineEventHandler } from 'h3'
3+
import { getRouterParam, createError, defineEventHandler } from 'h3'
44
import { useI18nContext } from '../context'
55
import { getMergedMessages } from '../utils/messages'
66

7+
import type { H3Event } from 'h3'
8+
79
/**
810
* Load messages for the specified locale event parameter
911
*/

src/runtime/server/utils/cache.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useStorage } from 'nitropack/runtime'
2+
import { prefixStorage } from 'unstorage'
3+
import { localeLoaders } from '#internal/i18n/options.mjs'
4+
5+
export interface CacheOptions<T = any, ArgsT extends unknown[] = any[]> {
6+
name?: string
7+
getKey: (...args: ArgsT) => string
8+
shouldBypassCache: (...args: ArgsT) => boolean
9+
group?: string
10+
/**
11+
* Number of seconds to cache the response. Defaults to 1.
12+
*/
13+
maxAge?: number
14+
}
15+
16+
const storage = prefixStorage(useStorage(), 'i18n')
17+
type CachedValue<T> = { ttl: number; value: T; mtime: number }
18+
19+
/**
20+
* Create a cached function
21+
* Adapted from nitropack/runtime `cachedFunction`
22+
*/
23+
export function cachedFunctionI18n<T, ArgsT extends unknown[] = any[]>(
24+
fn: (...args: ArgsT) => T | Promise<T>,
25+
opts: CacheOptions<T, ArgsT>
26+
): (...args: ArgsT) => Promise<T> {
27+
opts = { maxAge: 1, ...opts }
28+
const pending: { [key: string]: Promise<T> } = {}
29+
30+
async function get(key: string, resolver: () => T | Promise<T>) {
31+
const isPending = pending[key]
32+
33+
if (!isPending) {
34+
pending[key] = Promise.resolve(resolver())
35+
}
36+
37+
try {
38+
return await pending[key]
39+
} finally {
40+
// Ensure we always clean up, whether the promise resolved or rejected.
41+
delete pending[key]
42+
}
43+
}
44+
45+
return async (...args) => {
46+
const key = [opts.name, opts.getKey(...args)].join(':').replace(/:\/$/, ':index')
47+
const maxAge = opts.maxAge ?? 1
48+
const isCacheable = !opts.shouldBypassCache(...args) && maxAge >= 0
49+
50+
const cache = isCacheable && (await storage.getItemRaw<CachedValue<T>>(key))
51+
if (!cache || cache.ttl < Date.now()) {
52+
pending[key] = Promise.resolve(fn(...args))
53+
const value = await get(key, () => fn(...args))
54+
55+
if (isCacheable) {
56+
await storage.setItemRaw(key, { ttl: Date.now() + maxAge * 1000, value, mtime: Date.now() })
57+
}
58+
59+
return value
60+
}
61+
62+
return cache.value
63+
}
64+
}
65+
66+
/**
67+
* Check if the loaders for the specified locale are all cacheable
68+
*/
69+
export function isLocaleCacheable(locale: string) {
70+
return localeLoaders[locale] != null && localeLoaders[locale].every(loader => loader.cache !== false)
71+
}
72+
73+
/**
74+
* Check if the loaders for the specified locale and fallback locales are all cacheable
75+
*/
76+
export function isLocaleWithFallbacksCacheable(locale: string, fallbackLocales: string[]) {
77+
return isLocaleCacheable(locale) && fallbackLocales.every(fallbackLocale => isLocaleCacheable(fallbackLocale))
78+
}
Lines changed: 43 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { deepCopy } from '@intlify/shared'
2-
import { cachedFunction, useStorage } from 'nitropack/runtime'
32
import { localeLoaders } from '#internal/i18n/options.mjs'
43
import { getLocaleMessagesMerged } from '../../shared/messages'
4+
import { cachedFunctionI18n, isLocaleCacheable, isLocaleWithFallbacksCacheable } from './cache'
55

66
import type { LocaleMessages } from '@intlify/core'
77
import type { DefineLocaleMessage } from '@intlify/h3'
@@ -16,8 +16,8 @@ const _getMessages = async (locale: string) => {
1616
/**
1717
* Load messages for the specified locale (cached)
1818
*/
19-
const _getMessagesCached = cachedFunction(_getMessages, {
20-
name: 'i18n:loadMessages',
19+
const _getMessagesCached = cachedFunctionI18n(_getMessages, {
20+
name: 'messages',
2121
maxAge: !__I18N_CACHE__ ? -1 : 60 * 60 * 24,
2222
getKey: locale => locale,
2323
shouldBypassCache: locale => !isLocaleCacheable(locale)
@@ -30,77 +30,57 @@ const _getMessagesCached = cachedFunction(_getMessages, {
3030
*/
3131
const getMessages = import.meta.dev ? _getMessages : _getMessagesCached
3232

33-
const storage = useStorage('i18n')
34-
type MessagesCached = { ttl: number; value: LocaleMessages<DefineLocaleMessage> }
35-
/**
36-
* Load messages for the specified locale and merge with fallback locales in the shape of `{ [locale]: { ... } }`
37-
* @param locale - The locale to load messages for
38-
* @param fallbackLocales - The fallback locales to merge with
39-
*/
40-
export const getMergedMessages = async (locale: string, fallbackLocales: string[]) => {
41-
const cacheKey = `merged:${locale}-[${fallbackLocales.join('-')}]`
42-
const isCacheable = isLocaleWithFallbacksCacheable(locale, fallbackLocales)
33+
const _getMergedMessages = async (locale: string, fallbackLocales: string[]) => {
34+
const merged = {} as LocaleMessages<DefineLocaleMessage>
4335

44-
const cache = isCacheable && (await storage.getItemRaw<MessagesCached>(cacheKey))
45-
if (!cache || cache.ttl < Date.now()) {
46-
const merged = {} as LocaleMessages<DefineLocaleMessage>
47-
48-
try {
49-
if (fallbackLocales.length > 0) {
50-
const messages = await Promise.all(fallbackLocales.map(getMessages))
51-
for (const message of messages) {
52-
deepCopy(message, merged)
53-
}
36+
try {
37+
if (fallbackLocales.length > 0) {
38+
const messages = await Promise.all(fallbackLocales.map(getMessages))
39+
for (const message of messages) {
40+
deepCopy(message, merged)
5441
}
42+
}
5543

56-
const message = await getMessages(locale)
57-
deepCopy(message, merged)
44+
const message = await getMessages(locale)
45+
deepCopy(message, merged)
5846

59-
if (isCacheable) {
60-
await storage.setItemRaw(cacheKey, { ttl: Date.now() + 1000 * 5, value: merged })
61-
}
62-
return merged
63-
} catch (e) {
64-
throw new Error('Failed to merge messages: ' + (e as Error).message)
65-
}
47+
return merged
48+
} catch (e) {
49+
throw new Error('Failed to merge messages: ' + (e as Error).message)
6650
}
67-
68-
return cache.value
6951
}
7052

71-
export const getAllMergedMessages = async (locales: string[]) => {
72-
const cacheKey = `merged-full:${locales.join('-')}`
73-
const isCacheable = locales.every(locale => isLocaleCacheable(locale))
53+
/**
54+
* Load messages for the specified locale and merge with fallback locales in the shape of `{ [locale]: { ... } }`
55+
* @param locale - The locale to load messages for
56+
* @param fallbackLocales - The fallback locales to merge with
57+
*/
58+
export const getMergedMessages = cachedFunctionI18n(_getMergedMessages, {
59+
name: 'merged-single',
60+
maxAge: !__I18N_CACHE__ ? -1 : 60 * 60 * 24,
61+
getKey: (locale, fallbackLocales) => `${locale}-[${[...new Set(fallbackLocales)].sort().join('-')}]`,
62+
shouldBypassCache: (locale, fallbackLocales) => !isLocaleWithFallbacksCacheable(locale, fallbackLocales)
63+
})
7464

75-
const cache = isCacheable && (await storage.getItemRaw<MessagesCached>(cacheKey))
76-
if (!cache || cache.ttl < Date.now()) {
77-
const merged = {} as LocaleMessages<DefineLocaleMessage>
65+
const _getAllMergedMessages = async (locales: string[]) => {
66+
const merged = {} as LocaleMessages<DefineLocaleMessage>
7867

79-
try {
80-
const messages = await Promise.all(locales.map(getMessages))
81-
for (const message of messages) {
82-
deepCopy(message, merged)
83-
}
68+
try {
69+
const messages = await Promise.all(locales.map(getMessages))
8470

85-
if (isCacheable) {
86-
await storage.setItemRaw(cacheKey, { ttl: Date.now() + 1000 * 5, value: merged })
87-
}
88-
return merged
89-
} catch (e) {
90-
throw new Error('Failed to merge messages: ' + (e as Error).message)
71+
for (const message of messages) {
72+
deepCopy(message, merged)
9173
}
92-
}
9374

94-
return cache.value
95-
}
96-
97-
/**
98-
* Check if the loaders for the specified locale are all cacheable
99-
*/
100-
export function isLocaleCacheable(locale: string) {
101-
return localeLoaders[locale] != null && localeLoaders[locale].every(loader => loader.cache !== false)
75+
return merged
76+
} catch (e) {
77+
throw new Error('Failed to merge messages: ' + (e as Error).message)
78+
}
10279
}
10380

104-
export function isLocaleWithFallbacksCacheable(locale: string, fallbackLocales: string[]) {
105-
return isLocaleCacheable(locale) && fallbackLocales.every(fallbackLocale => isLocaleCacheable(fallbackLocale))
106-
}
81+
export const getAllMergedMessages = cachedFunctionI18n(_getAllMergedMessages, {
82+
name: 'merged-all',
83+
maxAge: !__I18N_CACHE__ ? -1 : 60 * 60 * 24,
84+
getKey: locales => locales.join('-'),
85+
shouldBypassCache: locales => !locales.every(locale => isLocaleCacheable(locale))
86+
})

0 commit comments

Comments
 (0)