Feat(): distributed caching (#13435)

RESOLVES CORE-1153

**What**
- This pr mainly lay the foundation the caching layer. It comes with a modules (built in memory cache) and a redis provider.
- Apply caching to few touch point to test

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-09-30 18:19:06 +02:00
committed by GitHub
parent 5b135a41fe
commit b9d6f73320
117 changed files with 5741 additions and 530 deletions
@@ -1,6 +1,7 @@
import {
GraphResultSet,
IIndexService,
MedusaContainer,
RemoteJoinerOptions,
RemoteJoinerQuery,
RemoteQueryFilters,
@@ -11,6 +12,7 @@ import {
RemoteQueryObjectFromStringResult,
} from "@medusajs/types"
import {
Cached,
MedusaError,
isObject,
remoteQueryObjectFromString,
@@ -19,12 +21,62 @@ import {
import { RemoteQuery } from "./remote-query"
import { toRemoteQuery } from "./to-remote-query"
function extractCacheOptions(option: string) {
return function extractKey(args: any[]) {
return args[1]?.cache?.[option]
}
}
function isCacheEnabled(args: any[]) {
const isEnabled = extractCacheOptions("enable")(args)
if (isEnabled === false) {
return false
}
return (
isEnabled === true ||
extractCacheOptions("key")(args) ||
extractCacheOptions("ttl")(args) ||
extractCacheOptions("tags")(args) ||
extractCacheOptions("autoInvalidate")(args) ||
extractCacheOptions("providers")(args)
)
}
const cacheDecoratorOptions = {
enable: isCacheEnabled,
key: async (args, cachingModule) => {
const key = extractCacheOptions("key")(args)
if (key) {
return key
}
const queryOptions = args[0]
const remoteJoinerOptions = args[1] ?? {}
const { initialData, cache, ...restOptions } = remoteJoinerOptions
const keyInput = {
queryOptions,
options: restOptions,
}
return await cachingModule.computeKey(keyInput)
},
ttl: extractCacheOptions("ttl"),
tags: extractCacheOptions("tags"),
autoInvalidate: extractCacheOptions("autoInvalidate"),
providers: extractCacheOptions("providers"),
container: function (this: Query) {
return this.container
},
}
/**
* API wrapper around the remoteQuery
*/
export class Query {
#remoteQuery: RemoteQuery
#indexModule: IIndexService
protected container: MedusaContainer
/**
* Method to wrap execution of the graph query for instrumentation
@@ -61,12 +113,15 @@ export class Query {
constructor({
remoteQuery,
indexModule,
container,
}: {
remoteQuery: RemoteQuery
indexModule: IIndexService
container: MedusaContainer
}) {
this.#remoteQuery = remoteQuery
this.#indexModule = indexModule
this.container = container
}
#unwrapQueryConfig(
@@ -151,6 +206,7 @@ export class Query {
* Graph function uses the remoteQuery under the hood and
* returns a result set
*/
@Cached(cacheDecoratorOptions)
async graph<const TEntry extends string>(
queryOptions: RemoteQueryInput<TEntry>,
options?: RemoteJoinerOptions
@@ -189,6 +245,7 @@ export class Query {
* Index function uses the Index module to query and hydrates the data with query.graph
* returns a result set
*/
@Cached(cacheDecoratorOptions)
async index<const TEntry extends string>(
queryOptions: RemoteQueryInput<TEntry> & {
joinFilters?: RemoteQueryFilters<TEntry>
@@ -266,13 +323,16 @@ export class Query {
export function createQuery({
remoteQuery,
indexModule,
container,
}: {
remoteQuery: RemoteQuery
indexModule: IIndexService
container: MedusaContainer
}) {
const query = new Query({
remoteQuery,
indexModule,
container,
})
function backwardCompatibleQuery(...args: any[]) {
@@ -69,8 +69,7 @@ export function toRemoteQuery<const TEntity extends string>(
}
if (QueryContext.isQueryContext(src)) {
const normalizedFilters = { ...src } as any
delete normalizedFilters.__type
const { __type, ...normalizedFilters } = src as any
const prop = "context"
@@ -100,7 +99,7 @@ export function toRemoteQuery<const TEntity extends string>(
}
// Process filters and context recursively
processNestedObjects(joinerQuery[entity], context)
processNestedObjects(joinerQuery[entity], context, true)
for (const field of fields) {
const fieldAsString = field as string