Skip to content

Commit

Permalink
docs(core): Add docs on caching
Browse files Browse the repository at this point in the history
Relates to #3043.
  • Loading branch information
michaelbromley committed Oct 31, 2024
1 parent 4507735 commit 7b0a26e
Show file tree
Hide file tree
Showing 84 changed files with 1,339 additions and 313 deletions.
5 changes: 4 additions & 1 deletion docs/docs/guides/deployment/horizontal-scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ In Vendure, both the server and the worker can be scaled horizontally. Scaling t
In order to run Vendure in a multi-instance configuration, there are some important configuration changes you'll need to make. The key consideration in configuring Vendure for this scenario is to ensure that any persistent state is managed externally from the Node process, and is shared by all instances. Namely:

* The JobQueue should be stored externally using the [DefaultJobQueuePlugin](/reference/typescript-api/job-queue/default-job-queue-plugin/) (which stores jobs in the database) or the [BullMQJobQueuePlugin](/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin) (which stores jobs in Redis), or some other custom JobQueueStrategy. **Note:** the BullMQJobQueuePlugin is much more efficient than the DefaultJobQueuePlugin, and is recommended for production applications.
* A custom [SessionCacheStrategy](/reference/typescript-api/auth/session-cache-strategy/) must be used which stores the session cache externally (such as in the database or Redis), since the default strategy stores the cache in-memory and will cause inconsistencies in multi-instance setups. [Example Redis-based SessionCacheStrategy](/reference/typescript-api/auth/session-cache-strategy/)
* An appropriate [CacheStrategy](/reference/typescript-api/cache/cache-strategy/) must be used which stores the cache externally. Both the [DefaultCachePlugin](/reference/typescript-api/cache/default-cache-plugin/) and the [RedisCachePlugin](/reference/typescript-api/cache/redis-cache-plugin/) are suitable
for multi-instance setups. The DefaultCachePlugin uses the database to store the cache data, which is simple and effective, while the RedisCachePlugin uses a Redis server to store the cache data and can have better performance characteristics.
* If you are on a version prior to v3.1, a custom [SessionCacheStrategy](/reference/typescript-api/auth/session-cache-strategy/) must be used which stores the session cache externally (such as in the database or Redis), since the default strategy stores the cache in-memory and will cause inconsistencies in multi-instance setups. [Example Redis-based SessionCacheStrategy](/reference/typescript-api/auth/session-cache-strategy/).
From v3.1 the session cache is handled by the underlying cache strategy, so you normally don't need to define a custom SessionCacheStrategy.
* When using cookies to manage sessions, make sure all instances are using the _same_ cookie secret:
```ts title="src/vendure-config.ts"
const config: VendureConfig = {
Expand Down
206 changes: 206 additions & 0 deletions docs/docs/guides/developer-guide/cache/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
title: "Cache"
---

Caching is a technique to improve performance of a system by saving the results of expensive
operations and reusing them when the same operation is requested again.

Vendure uses caching in a number of places to improve performance, and the same caching
mechanism is available for use in custom plugins.

## CacheService

The [`CacheService`](/reference/typescript-api/cache/cache-service) is the general-purpose API for interacting with the cache.
It provides methods for setting, getting and deleting cache entries.

![CacheService](./cache-service.webp)

Internally, the `CacheService` uses a [CacheStrategy](/reference/typescript-api/cache/cache-strategy) to store the data. The cache strategy is responsible for
the actual storage and retrieval of the data. The `CacheService` provides a consistent API which can be used
regardless of the underlying cache strategy.

:::info
From Vendure v3.1, new projects are created with the [DefaultCachePlugin](/reference/typescript-api/cache/default-cache-plugin) enabled by default. This plugin
uses the database to store the cache data. This is a simple and effective cache strategy which is suitable
for most use-cases.

For more advanced use-cases, you can use the [RedisCachePlugin](/reference/typescript-api/cache/redis-cache-plugin) which uses a Redis
server to store the cache data and can have better performance characteristics.
:::

### Multi-instance use

It is common to run Vendure in a multi-instance setup, where multiple instances of the server and worker are
running in parallel.

The `CacheService` is designed to work in this environment. Both the [DefaultCachePlugin](/reference/typescript-api/cache/default-cache-plugin)
and the [RedisCachePlugin](/reference/typescript-api/cache/redis-cache-plugin) use a single shared cache across all
instances.

This means that if one instance sets a cache entry, it will be available to all other instances. Likewise,
if one instance deletes a cache entry, it will be deleted for all other instances.

### Usage

The `CacheService` can be injected into any service, resolver, strategy or configurable operation.

```ts
import { Injectable } from '@nestjs/common';
import { CacheService } from '@vendure/core';

@Injectable()
export class MyService {
constructor(private cacheService: CacheService) {}

async myMethod() {
const cacheKey = 'MyService.myMethod';
const cachedValue = await this.cacheService.get(cacheKey);
if (cachedValue) {
return cachedValue;
}
const newValue = await this.expensiveOperation();
// Cache the result for 1 minute (60 * 1000 milliseconds)
await this.cacheService.set(cacheKey, newValue, { ttl: 60 * 1000 });
return newValue;
}

private async expensiveOperation() {
// Do something expensive
}
}
```

:::info

The data stored in the cache must be serializable. This means you cannot store instances of classes,
functions, or other non-serializable data types.

:::

### Cache key naming

When setting a cache entry, it is important to choose a unique key which will not conflict
with other cache entries. The key should be namespaced to avoid conflicts. For example,
you can use the name of the class & method as part of the key. If there is an identifier
which is unique to the operation, that can be used as well.

```ts
getVariantIds(productId: ID): Promise<ID[]> {
const cacheKey = `ProductService.getVariantIds:${productId}`;
const cachedValue = await this.cacheService.get(cacheKey);
if (cachedValue) {
return cachedValue;
}
const newValue = await this.expensiveOperation(productId);
await this.cacheService.set(cacheKey, newValue, { ttl: 60 * 1000 });
return newValue;
}
```

### Cache eviction

The cache is not infinite, and entries will be evicted after a certain time. The time-to-live (TTL)
of a cache entry can be set when calling `set()`. If no TTL is set, the cache entry will remain
in the cache indefinitely.

Cache entries can also be manually deleted using the `delete()` method:

```ts
await this.cacheService.delete(cacheKey);
```

### Cache tags

When setting a cache entry, you can also specify a list of tags. This allows you to invalidate
all cache entries which share a tag. For example, if you have a cache entry which is related to
a Product, you can tag it with the Product's ID. When the Product is updated, you can invalidate
all cache entries which are tagged with that Product ID.

```ts
const cacheKey = `ProductService.getVariantIds:${productId}`;

await this.cacheService.set(cacheKey, newValue, {
tags: [`Product:${productId}`]
});

// later

await this.cacheService.invalidateTags([`Product:${productId}`]);
```

### createCache Helper

The `createCache` helper function can be used to create a [Cache](/reference/typescript-api/cache) instance
which is a convenience wrapper around the `CacheService` APIs:

```ts
import { Injectable } from '@nestjs/common';
import { CacheService, ID, EventBus, ProductEvent,RequestContext } from '@vendure/core';

@Injectable()
export class FacetValueChecker {
// Create a Cache instance with a 1-day TTL
private facetValueCache = this.cacheService.createCache({
getKey: (productId: ID) => `FacetValueChecker.${productId}`,
options: { ttl: 1000 * 60 * 60 * 24 },
});

constructor(private cacheService: CacheService, private eventBus: EventBus) {
this.eventBus.ofType(ProductEvent).subscribe(event => {
if (event.type !== 'created') {
// Invalidate the cache entry when a Product is updated or deleted
this.facetValueCache.delete(event.entity.id);
}
});
}

async getFacetValueIdsForProduct(ctx: RequestContext, productId: ID): Promise<ID[]> {
return this.facetValueCache.get(productId, () =>
// This function will only be called if the cache entry does not exist
// or has expired. It will set the result in the cache automatically.
this.calculateFacetValueIdsForProduct(ctx, productId));
}

async calculateFacetValueIdsForProduct(ctx: RequestContext, productId: ID): Promise<ID[]> {
// Do something expensive
}
}
```

## RequestContextCache

The [RequestContextCacheService](/reference/typescript-api/cache/request-context-cache-service) is a specialized
cache service which is scoped to the current request. This is useful when you want to cache data
for the duration of a single request, but not across multiple requests.

This can be especially useful in resolvers, where you may want to cache the result of a specific resolved
field which may be requested multiple times within the same request.

For example, in Vendure core, when dealing with product lists, there's a particular very hot
code path that is used to calculate the correct prices to return for each product. As part of this
calculation, we need to know the active tax zone, which can be expensive to calculate newly
for each product. We use the `RequestContextCacheService` to cache the active tax zone for the
duration of the request.

```ts
const activeTaxZone = await this.requestContextCache.get(
ctx,
'activeTaxZone',
() => taxZoneStrategy
.determineTaxZone(ctx, zones, ctx.channel, order),
);
```

Internally, the `RequestContextCacheService` makes used of the WeakMap data structure which means the cached
data will be automatically garbage-collected when the request is finished. It is also able to store
any kind of data, not just serializable data.

## Session Cache

There is an additional cache which is specifically used to cache session data, since this data is commonly
accessed on almost all requests. Since v3.1, the default is to use the [DefaultSessionCacheStrategy](/reference/typescript-api/cache/default-session-cache-strategy)
which internally just uses whatever the current `CacheStrategy` is to store the data.

This means that in most cases you don't need to worry about the session cache, but if you have specific
requirements, you can create a custom session cache strategy and set it via the `authOptions.sessionCacheStrategy`
config property.
28 changes: 14 additions & 14 deletions docs/docs/reference/admin-ui-api/alerts/alert-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';

## AlertConfig

<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts" sourceLine="62" packageName="@vendure/admin-ui" since="2.2.0" />
<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts" sourceLine="63" packageName="@vendure/admin-ui" since="2.2.0" />

A configuration object for an Admin UI alert.

Expand All @@ -22,9 +22,9 @@ interface AlertConfig<T = any> {
recheck?: (context: AlertContext) => Observable<any>;
isAlert: (data: T, context: AlertContext) => boolean;
action: (data: T, context: AlertContext) => void;
label: (
data: T,
context: AlertContext,
label: (
data: T,
context: AlertContext,
) => { text: string; translationVars?: { [key: string]: string | number } };
requiredPermissions?: Permission[];
}
Expand All @@ -41,18 +41,18 @@ A unique identifier for the alert.

<MemberInfo kind="property" type={`(context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; T | Promise&#60;T&#62; | Observable&#60;T&#62;`} />

A function which is gets the data used to determine whether the alert should be shown.
Typically, this function will query the server or some other remote data source.

This function will be called once when the Admin UI app bootstraps, and can be also
A function which is gets the data used to determine whether the alert should be shown.
Typically, this function will query the server or some other remote data source.

This function will be called once when the Admin UI app bootstraps, and can be also
set to run at regular intervals by setting the `recheckIntervalMs` property.
### recheck

<MemberInfo kind="property" type={`(context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; Observable&#60;any&#62;`} default={`undefined`} />

A function which returns an Observable which is used to determine when to re-run the `check`
function. Whenever the observable emits, the `check` function will be called again.

A function which returns an Observable which is used to determine when to re-run the `check`
function. Whenever the observable emits, the `check` function will be called again.

A basic time-interval-based recheck can be achieved by using the `interval` function from RxJS.

*Example*
Expand All @@ -69,7 +69,7 @@ If this is not set, the `check` function will only be called once when the Admin

<MemberInfo kind="property" type={`(data: T, context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; boolean`} />

A function which determines whether the alert should be shown based on the data returned by the `check`
A function which determines whether the alert should be shown based on the data returned by the `check`
function.
### action

Expand All @@ -78,14 +78,14 @@ function.
A function which is called when the alert is clicked in the Admin UI.
### label

<MemberInfo kind="property" type={`( data: T, context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>, ) =&#62; { text: string; translationVars?: { [key: string]: string | number } }`} />
<MemberInfo kind="property" type={`( data: T, context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>, ) =&#62; { text: string; translationVars?: { [key: string]: string | number } }`} />

A function which returns the text used in the UI to describe the alert.
### requiredPermissions

<MemberInfo kind="property" type={`<a href='/reference/typescript-api/common/permission#permission'>Permission</a>[]`} />

A list of permissions which the current Administrator must have in order. If the current
A list of permissions which the current Administrator must have in order. If the current
Administrator does not have these permissions, none of the other alert functions will be called.


Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/admin-ui-api/alerts/alert-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';

## AlertContext

<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts" sourceLine="28" packageName="@vendure/admin-ui" since="2.2.0" />
<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts" sourceLine="29" packageName="@vendure/admin-ui" since="2.2.0" />

The context object which is passed to the `check`, `isAlert`, `label` and `action` functions of an
<a href='/reference/admin-ui-api/alerts/alert-config#alertconfig'>AlertConfig</a> object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';

## AssetPickerDialogComponent

<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.ts" sourceLine="52" packageName="@vendure/admin-ui" />
<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.ts" sourceLine="51" packageName="@vendure/admin-ui" />

A dialog which allows the creation and selection of assets.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
@Input() activeIndex = -1;
@Output() pageChange = new EventEmitter<number>();
@Output() itemsPerPageChange = new EventEmitter<number>();
@Output() visibleColumnsChange = new EventEmitter<Array<DataTable2ColumnComponent<T>>>();
@ContentChildren(DataTable2ColumnComponent) columns: QueryList<DataTable2ColumnComponent<T>>;
@ContentChildren(DataTableCustomFieldColumnComponent)
customFieldColumns: QueryList<DataTableCustomFieldColumnComponent<T>>;
Expand All @@ -95,6 +96,7 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
route = inject(ActivatedRoute);
filterPresetService = inject(FilterPresetService);
dataTableCustomComponentService = inject(DataTableCustomComponentService);
dataTableConfigService = inject(DataTableConfigService);
protected customComponents = new Map<string, { config: DataTableComponentConfig; injector: Injector }>();
rowTemplate: TemplateRef<any>;
currentStart: number;
Expand All @@ -103,7 +105,7 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
showSearchFilterRow = false;
protected uiLanguage$: Observable<LanguageCode>;
protected destroy$ = new Subject<void>();
constructor(changeDetectorRef: ChangeDetectorRef, localStorageService: LocalStorageService, dataService: DataService)
constructor(changeDetectorRef: ChangeDetectorRef, dataService: DataService)
selectionManager: void
allColumns: void
visibleSortedColumns: void
Expand All @@ -117,7 +119,6 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
trackByFn(index: number, item: any) => ;
onToggleAllClick() => ;
onRowClick(item: T, event: MouseEvent) => ;
getDataTableConfig() => DataTableConfig;
}
```
* Implements: <code>AfterContentInit</code>, <code>OnChanges</code>, <code>OnDestroy</code>
Expand Down Expand Up @@ -176,6 +177,11 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
<MemberInfo kind="property" type={``} />


### visibleColumnsChange

<MemberInfo kind="property" type={``} />


### columns

<MemberInfo kind="property" type={`QueryList&#60;DataTable2ColumnComponent&#60;T&#62;&#62;`} />
Expand Down Expand Up @@ -226,6 +232,11 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
<MemberInfo kind="property" type={``} />


### dataTableConfigService

<MemberInfo kind="property" type={``} />


### customComponents

<MemberInfo kind="property" type={``} />
Expand Down Expand Up @@ -268,7 +279,7 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {

### constructor

<MemberInfo kind="method" type={`(changeDetectorRef: ChangeDetectorRef, localStorageService: LocalStorageService, dataService: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>) => DataTable2Component`} />
<MemberInfo kind="method" type={`(changeDetectorRef: ChangeDetectorRef, dataService: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>) => DataTable2Component`} />


### selectionManager
Expand Down Expand Up @@ -336,11 +347,6 @@ class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
<MemberInfo kind="method" type={`(item: T, event: MouseEvent) => `} />


### getDataTableConfig

<MemberInfo kind="method" type={`() => DataTableConfig`} />




</div>
Loading

0 comments on commit 7b0a26e

Please sign in to comment.