@breadstone/archipel-mcp 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/guides/ai-text-generation.md +361 -0
- package/data/guides/analytics-and-error-tracking.md +189 -0
- package/data/guides/authentication-and-authorization.md +657 -0
- package/data/guides/blob-storage.md +242 -0
- package/data/guides/caching.md +255 -0
- package/data/guides/cryptography-and-otp.md +240 -0
- package/data/guides/document-generation.md +174 -0
- package/data/guides/email-delivery.md +196 -0
- package/data/guides/esigning-integration.md +231 -0
- package/data/guides/getting-started.md +351 -0
- package/data/guides/implementing-ports.md +317 -0
- package/data/guides/index.md +61 -0
- package/data/guides/mcp-server.md +222 -0
- package/data/guides/openapi-and-feature-discovery.md +266 -0
- package/data/guides/payments-and-feature-gating.md +244 -0
- package/data/guides/resource-management.md +352 -0
- package/data/guides/telemetry-and-observability.md +190 -0
- package/data/guides/testing.md +319 -0
- package/data/guides/tsdoc-guidelines.md +45 -0
- package/data/packages/platform-openapi/api/Class.SwaggerFeatureDiscovery.md +10 -6
- package/package.json +1 -1
- package/src/GuidesLoader.d.ts +13 -0
- package/src/GuidesLoader.js +81 -0
- package/src/main.js +36 -198
- package/src/models/IGuideDoc.d.ts +15 -0
- package/src/models/IGuideDoc.js +3 -0
- package/src/tools/registerGetConfigPatternTool.d.ts +5 -0
- package/src/tools/registerGetConfigPatternTool.js +15 -0
- package/src/tools/registerGetDtoPatternTool.d.ts +5 -0
- package/src/tools/registerGetDtoPatternTool.js +15 -0
- package/src/tools/registerGetErrorHandlingPatternTool.d.ts +5 -0
- package/src/tools/registerGetErrorHandlingPatternTool.js +15 -0
- package/src/tools/registerGetGuardPatternTool.d.ts +5 -0
- package/src/tools/registerGetGuardPatternTool.js +15 -0
- package/src/tools/registerGetGuideTool.d.ts +6 -0
- package/src/tools/registerGetGuideTool.js +32 -0
- package/src/tools/registerGetMappingPatternTool.d.ts +5 -0
- package/src/tools/registerGetMappingPatternTool.js +34 -0
- package/src/tools/registerGetModulePatternTool.d.ts +5 -0
- package/src/tools/registerGetModulePatternTool.js +29 -0
- package/src/tools/registerGetPackageDocTool.d.ts +6 -0
- package/src/tools/registerGetPackageDocTool.js +43 -0
- package/src/tools/registerGetQueryPatternTool.d.ts +5 -0
- package/src/tools/registerGetQueryPatternTool.js +29 -0
- package/src/tools/registerGetRepositoryPatternTool.d.ts +5 -0
- package/src/tools/registerGetRepositoryPatternTool.js +29 -0
- package/src/tools/registerGetTestingPatternTool.d.ts +5 -0
- package/src/tools/registerGetTestingPatternTool.js +15 -0
- package/src/tools/registerListGuidesTool.d.ts +6 -0
- package/src/tools/registerListGuidesTool.js +22 -0
- package/src/tools/registerListPackagesTool.d.ts +6 -0
- package/src/tools/registerListPackagesTool.js +20 -0
- package/src/tools/registerSearchDocsTool.d.ts +6 -0
- package/src/tools/registerSearchDocsTool.js +35 -0
- package/src/tools/registerSearchGuidesTool.d.ts +6 -0
- package/src/tools/registerSearchGuidesTool.js +28 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Blob Storage
|
|
3
|
+
description: Upload, download, and manage files with multi-provider blob storage using Vercel, Azure, AWS S3, or custom providers.
|
|
4
|
+
order: 7
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Blob Storage
|
|
8
|
+
|
|
9
|
+
This guide covers file storage with `platform-blob-storage`: choosing a provider, uploading and downloading files, generating signed URLs, and optionally tracking file metadata with persistence ports.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @breadstone/archipel-platform-blob-storage
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Choose a Provider
|
|
22
|
+
|
|
23
|
+
`platform-blob-storage` abstracts file operations behind a unified `BlobService`. You select the provider at module registration time. Supported providers:
|
|
24
|
+
|
|
25
|
+
| Provider | Kind constant | When to use |
|
|
26
|
+
| ---------- | -------------------------- | ------------------------------------------- |
|
|
27
|
+
| **Vercel** | `BlobProviderKinds.Vercel` | Vercel-hosted apps with Vercel Blob storage |
|
|
28
|
+
| **Azure** | `BlobProviderKinds.Azure` | Azure Blob Storage accounts |
|
|
29
|
+
| **AWS S3** | `BlobProviderKinds.AwsS3` | Amazon S3 or S3-compatible providers |
|
|
30
|
+
| **Custom** | `BlobProviderKinds.Custom` | Your own storage backend |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Module Registration
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Module } from '@nestjs/common';
|
|
38
|
+
import { BlobModule, BlobProviderKinds } from '@breadstone/archipel-platform-blob-storage';
|
|
39
|
+
|
|
40
|
+
@Module({
|
|
41
|
+
imports: [
|
|
42
|
+
BlobModule.forRoot({
|
|
43
|
+
provider: {
|
|
44
|
+
kind: BlobProviderKinds.AwsS3,
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
export class AppModule {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each provider reads its credentials from environment variables via the `ConfigModule`. Set the variables for your chosen provider:
|
|
53
|
+
|
|
54
|
+
### AWS S3
|
|
55
|
+
|
|
56
|
+
```env
|
|
57
|
+
BLOB_AWS_REGION=eu-central-1
|
|
58
|
+
BLOB_AWS_BUCKET=my-app-uploads
|
|
59
|
+
BLOB_AWS_ACCESS_KEY_ID=AKIA...
|
|
60
|
+
BLOB_AWS_SECRET_ACCESS_KEY=secret...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Azure Blob Storage
|
|
64
|
+
|
|
65
|
+
```env
|
|
66
|
+
BLOB_AZURE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
|
|
67
|
+
BLOB_AZURE_CONTAINER=uploads
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Vercel Blob
|
|
71
|
+
|
|
72
|
+
```env
|
|
73
|
+
BLOB_VERCEL_TOKEN=your-vercel-blob-token
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Uploading Files
|
|
79
|
+
|
|
80
|
+
Inject `BlobService` to upload files. It returns metadata about the stored blob.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Injectable } from '@nestjs/common';
|
|
84
|
+
import { BlobService } from '@breadstone/archipel-platform-blob-storage';
|
|
85
|
+
|
|
86
|
+
@Injectable()
|
|
87
|
+
export class AvatarService {
|
|
88
|
+
private readonly _blobService: BlobService;
|
|
89
|
+
|
|
90
|
+
constructor(blobService: BlobService) {
|
|
91
|
+
this._blobService = blobService;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async uploadAvatar(userId: string, file: Buffer, mimeType: string): Promise<string> {
|
|
95
|
+
const result = await this._blobService.upload({
|
|
96
|
+
key: `avatars/${userId}`,
|
|
97
|
+
body: file,
|
|
98
|
+
contentType: mimeType,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return result.url;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Handling Multipart Uploads in Controllers
|
|
107
|
+
|
|
108
|
+
For HTTP file uploads, use the `@UploadedFile()` decorator with Multer:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
|
|
112
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
113
|
+
import { JwtAuthGuard, User, type IAuthSubject } from '@breadstone/archipel-platform-authentication';
|
|
114
|
+
|
|
115
|
+
@Controller('avatars')
|
|
116
|
+
export class AvatarController {
|
|
117
|
+
private readonly _avatarService: AvatarService;
|
|
118
|
+
|
|
119
|
+
constructor(avatarService: AvatarService) {
|
|
120
|
+
this._avatarService = avatarService;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Post()
|
|
124
|
+
@UseGuards(JwtAuthGuard)
|
|
125
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
126
|
+
public async upload(@User() user: IAuthSubject, @UploadedFile() file: Express.Multer.File): Promise<{ url: string }> {
|
|
127
|
+
const url = await this._avatarService.uploadAvatar(user.id, file.buffer, file.mimetype);
|
|
128
|
+
return { url };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Downloading and Signed URLs
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Download a blob
|
|
139
|
+
const content = await this._blobService.download('avatars/user-123');
|
|
140
|
+
|
|
141
|
+
// Generate a time-limited signed URL for direct client access
|
|
142
|
+
const signedUrl = await this._blobService.getSignedUrl('avatars/user-123', {
|
|
143
|
+
expiresIn: 3600, // seconds
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Signed URLs are useful for serving private files directly to the client without proxying through your server.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Deleting Files
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
await this._blobService.delete('avatars/user-123');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Tracking Metadata (Optional Ports)
|
|
160
|
+
|
|
161
|
+
By default, `BlobService` performs storage operations without persisting metadata to your database. If you need to track uploaded files (for listing, auditing, or cleanup), implement the optional persistence ports.
|
|
162
|
+
|
|
163
|
+
### BlobObjectPersistencePort
|
|
164
|
+
|
|
165
|
+
Tracks blob metadata after upload and deletion:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { Injectable } from '@nestjs/common';
|
|
169
|
+
import { BlobObjectPersistencePort } from '@breadstone/archipel-platform-blob-storage';
|
|
170
|
+
import { PrismaService } from '@breadstone/archipel-platform-database';
|
|
171
|
+
|
|
172
|
+
@Injectable()
|
|
173
|
+
export class PrismaBlobObjectAdapter extends BlobObjectPersistencePort {
|
|
174
|
+
private readonly _prisma: PrismaService;
|
|
175
|
+
|
|
176
|
+
constructor(prisma: PrismaService) {
|
|
177
|
+
super();
|
|
178
|
+
this._prisma = prisma;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async onUploaded(key: string, url: string, contentType: string, size: number): Promise<void> {
|
|
182
|
+
await this._prisma.blobObject.upsert({
|
|
183
|
+
where: { key },
|
|
184
|
+
create: { key, url, contentType, size },
|
|
185
|
+
update: { url, contentType, size },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public async onDeleted(key: string): Promise<void> {
|
|
190
|
+
await this._prisma.blobObject.deleteMany({ where: { key } });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### BlobVariantPersistencePort
|
|
196
|
+
|
|
197
|
+
Tracks image variants (thumbnails, resized versions):
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { Injectable } from '@nestjs/common';
|
|
201
|
+
import { BlobVariantPersistencePort } from '@breadstone/archipel-platform-blob-storage';
|
|
202
|
+
import { PrismaService } from '@breadstone/archipel-platform-database';
|
|
203
|
+
|
|
204
|
+
@Injectable()
|
|
205
|
+
export class PrismaBlobVariantAdapter extends BlobVariantPersistencePort {
|
|
206
|
+
private readonly _prisma: PrismaService;
|
|
207
|
+
|
|
208
|
+
constructor(prisma: PrismaService) {
|
|
209
|
+
super();
|
|
210
|
+
this._prisma = prisma;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public async onVariantCreated(originalKey: string, variantKey: string, width: number, height: number): Promise<void> {
|
|
214
|
+
await this._prisma.blobVariant.create({
|
|
215
|
+
data: { originalKey, variantKey, width, height },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Register with Persistence
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
BlobModule.forRoot({
|
|
225
|
+
provider: { kind: BlobProviderKinds.AwsS3 },
|
|
226
|
+
objectPersistence: PrismaBlobObjectAdapter,
|
|
227
|
+
variantPersistence: PrismaBlobVariantAdapter,
|
|
228
|
+
}),
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Health Checks
|
|
234
|
+
|
|
235
|
+
`platform-blob-storage` registers a health indicator automatically. It verifies connectivity to the configured provider. The result is included in your `/health` endpoint if you use `platform-core`'s health module.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Next Steps
|
|
240
|
+
|
|
241
|
+
- See the [Implementing Ports](/guides/implementing-ports) guide for the general port/adapter pattern
|
|
242
|
+
- Browse the [platform-blob-storage API reference](/packages/platform-blob-storage/api/) for all method signatures
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Caching
|
|
3
|
+
description: Add layered caching to your NestJS application with in-memory LRU and Redis-backed implementations, TTL expiration, stale-while-revalidate, and metrics.
|
|
4
|
+
order: 16
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Caching
|
|
8
|
+
|
|
9
|
+
This guide covers layered caching with `platform-caching`: choosing between in-memory and Redis backends, configuring TTL and LRU eviction, using stale-while-revalidate for low-latency reads, and integrating cache metrics with your observability stack.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @breadstone/archipel-platform-caching
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For Redis-backed caching, also install `ioredis`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add ioredis
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Concepts
|
|
28
|
+
|
|
29
|
+
`platform-caching` provides a generic `ILayeredCache<TKey, TValue>` interface with two implementations:
|
|
30
|
+
|
|
31
|
+
| Implementation | Backend | Sync support | Best for |
|
|
32
|
+
| -------------------- | --------- | ------------ | ---------------------------------------- |
|
|
33
|
+
| `MemoryLayeredCache` | In-memory | Yes | Single-instance apps, fast local lookups |
|
|
34
|
+
| `RedisLayeredCache` | Redis | No (async) | Multi-instance deployments, shared state |
|
|
35
|
+
|
|
36
|
+
Both implementations use a **loader function** pattern: when a cache miss occurs, the cache automatically calls a user-provided async function to load the value, stores it, and returns it. This eliminates repetitive "check cache → miss → load → store" boilerplate.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## In-Memory Cache
|
|
41
|
+
|
|
42
|
+
### Basic Setup
|
|
43
|
+
|
|
44
|
+
Create a `MemoryLayeredCache` with a loader function that fetches the value on cache misses:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { Injectable } from '@nestjs/common';
|
|
48
|
+
import { MemoryLayeredCache } from '@breadstone/archipel-platform-caching';
|
|
49
|
+
|
|
50
|
+
@Injectable()
|
|
51
|
+
export class ProductCacheService {
|
|
52
|
+
private readonly _cache: MemoryLayeredCache<string, IProduct>;
|
|
53
|
+
|
|
54
|
+
constructor(productRepository: ProductRepository) {
|
|
55
|
+
this._cache = new MemoryLayeredCache<string, IProduct>(async (productId: string) =>
|
|
56
|
+
productRepository.findById(productId),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async getProduct(productId: string): Promise<IProduct | undefined> {
|
|
61
|
+
return this._cache.getAsync(productId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public invalidateProduct(productId: string): void {
|
|
65
|
+
this._cache.invalidate(productId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### TTL Expiration
|
|
71
|
+
|
|
72
|
+
Set a time-to-live so entries automatically expire:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
this._cache = new MemoryLayeredCache<string, IProduct>(
|
|
76
|
+
async (id) => productRepository.findById(id),
|
|
77
|
+
{ ttlMs: 60_000 }, // entries expire after 60 seconds
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### LRU Eviction
|
|
82
|
+
|
|
83
|
+
Limit the number of entries to prevent unbounded memory growth. The oldest entry (least recently used) is evicted first:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
this._cache = new MemoryLayeredCache<string, IProduct>(async (id) => productRepository.findById(id), {
|
|
87
|
+
ttlMs: 300_000, // 5-minute TTL
|
|
88
|
+
maxEntries: 1000, // keep at most 1000 entries
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Stale-While-Revalidate
|
|
93
|
+
|
|
94
|
+
Serve expired entries immediately while refreshing in the background. This keeps latency low for frequently accessed data:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
this._cache = new MemoryLayeredCache<string, IProduct>(async (id) => productRepository.findById(id), {
|
|
98
|
+
ttlMs: 30_000,
|
|
99
|
+
staleWhileRevalidate: true, // return stale value, refresh async
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> **When to use:** Quasi-static reference data (categories, feature flags, configuration). Avoid for personalized or rapidly changing data where freshness is critical.
|
|
104
|
+
|
|
105
|
+
### Synchronous Access
|
|
106
|
+
|
|
107
|
+
`MemoryLayeredCache` supports synchronous `get()` and `set()` for cases where you've pre-warmed the cache or need zero-await lookups:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Pre-warm on startup
|
|
111
|
+
await this._cache.warm('featured-product');
|
|
112
|
+
|
|
113
|
+
// Later: synchronous read (no await needed)
|
|
114
|
+
const product = this._cache.get('featured-product');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Redis Cache
|
|
120
|
+
|
|
121
|
+
### Basic Setup
|
|
122
|
+
|
|
123
|
+
`RedisLayeredCache` provides the same `ILayeredCache` interface but stores entries in Redis. Synchronous methods (`get`, `set`, `invalidate`) throw — use the async variants.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { Injectable } from '@nestjs/common';
|
|
127
|
+
import { RedisLayeredCache } from '@breadstone/archipel-platform-caching/redis';
|
|
128
|
+
|
|
129
|
+
@Injectable()
|
|
130
|
+
export class SessionCacheService {
|
|
131
|
+
private readonly _cache: RedisLayeredCache<string, ISessionData>;
|
|
132
|
+
|
|
133
|
+
constructor(sessionRepository: SessionRepository) {
|
|
134
|
+
this._cache = new RedisLayeredCache<string, ISessionData>(
|
|
135
|
+
async (sessionId) => sessionRepository.findById(sessionId),
|
|
136
|
+
{
|
|
137
|
+
url: process.env['REDIS_URL'] ?? 'redis://localhost:6379',
|
|
138
|
+
keyPrefix: 'session:',
|
|
139
|
+
ttlSeconds: 3600, // 1-hour TTL
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public async getSession(sessionId: string): Promise<ISessionData | undefined> {
|
|
145
|
+
return this._cache.getAsync(sessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async invalidateSession(sessionId: string): Promise<void> {
|
|
149
|
+
await this._cache.invalidateAsync(sessionId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom Serialization
|
|
155
|
+
|
|
156
|
+
By default, values are serialized with `JSON.stringify` and deserialized with `JSON.parse`. Override this for custom formats:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
this._cache = new RedisLayeredCache<string, ISessionData>(async (id) => sessionRepository.findById(id), {
|
|
160
|
+
url: 'redis://localhost:6379',
|
|
161
|
+
keyPrefix: 'session:',
|
|
162
|
+
serialize: (value) => msgpack.encode(value).toString('base64'),
|
|
163
|
+
deserialize: (raw) => msgpack.decode(Buffer.from(raw, 'base64')) as ISessionData,
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Graceful Shutdown
|
|
168
|
+
|
|
169
|
+
`RedisLayeredCache` implements `OnModuleDestroy` and automatically disconnects the Redis client when the NestJS module is destroyed. No manual cleanup is needed.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Cache Metrics
|
|
174
|
+
|
|
175
|
+
Both implementations accept an optional `ICacheMetricsRecorder` to forward cache statistics to your observability backend.
|
|
176
|
+
|
|
177
|
+
### Implementing a Metrics Recorder
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import type { ICacheMetricsRecorder } from '@breadstone/archipel-platform-caching';
|
|
181
|
+
import { TelemetryFacade } from '@breadstone/archipel-platform-telemetry';
|
|
182
|
+
|
|
183
|
+
export class TelemetryCacheMetricsRecorder implements ICacheMetricsRecorder {
|
|
184
|
+
private readonly _telemetry: TelemetryFacade;
|
|
185
|
+
|
|
186
|
+
constructor(telemetry: TelemetryFacade) {
|
|
187
|
+
this._telemetry = telemetry;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public recordHit(cache: string): void {
|
|
191
|
+
this._telemetry.recordCounter('cache_hits_total', 1, { cache });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public recordMiss(cache: string): void {
|
|
195
|
+
this._telemetry.recordCounter('cache_misses_total', 1, { cache });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public recordLoadSuccess(cache: string): void {
|
|
199
|
+
this._telemetry.recordCounter('cache_load_success_total', 1, { cache });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public recordLoadError(cache: string): void {
|
|
203
|
+
this._telemetry.recordCounter('cache_load_error_total', 1, { cache });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public recordEviction(cache: string): void {
|
|
207
|
+
this._telemetry.recordCounter('cache_evictions_total', 1, { cache });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Wiring It Up
|
|
213
|
+
|
|
214
|
+
Pass the recorder when creating the cache:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
this._cache = new MemoryLayeredCache<string, IProduct>(async (id) => productRepository.findById(id), {
|
|
218
|
+
ttlMs: 60_000,
|
|
219
|
+
maxEntries: 500,
|
|
220
|
+
metricsRecorder: new TelemetryCacheMetricsRecorder(telemetry),
|
|
221
|
+
cacheName: 'products',
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
When no metrics recorder is provided, a `NoopCacheMetricsRecorder` is used automatically — no setup needed.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Cache Statistics
|
|
230
|
+
|
|
231
|
+
Both implementations expose a `stats()` method returning a snapshot of cache performance:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const stats = this._cache.stats();
|
|
235
|
+
console.log(stats.hits); // total cache hits
|
|
236
|
+
console.log(stats.misses); // total cache misses
|
|
237
|
+
console.log(stats.loads); // successful loader invocations
|
|
238
|
+
console.log(stats.loadErrors); // failed loader invocations
|
|
239
|
+
console.log(stats.evictions); // LRU evictions (MemoryLayeredCache only)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Use this for health checks or debug endpoints.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Best Practices
|
|
247
|
+
|
|
248
|
+
| Practice | Why |
|
|
249
|
+
| ----------------------------------------- | ------------------------------------------------------------------ |
|
|
250
|
+
| Set `maxEntries` for in-memory caches | Prevents unbounded memory growth in long-running processes |
|
|
251
|
+
| Use `staleWhileRevalidate` for read-heavy | Keeps p99 latency low for popular keys |
|
|
252
|
+
| Invalidate on writes | Call `invalidate(key)` after mutations to avoid serving stale data |
|
|
253
|
+
| Use `cacheName` labels | Makes metrics dashboards filterable by cache instance |
|
|
254
|
+
| Keep TTLs short for user-specific data | Personalized data should not be stale for extended periods |
|
|
255
|
+
| Use Redis for multi-instance deployments | In-memory caches are not shared across application instances |
|