@checkstack/backend 0.6.6 → 0.7.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,127 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [208ad71]
8
+ - @checkstack/signal-common@0.2.0
9
+ - @checkstack/signal-backend@0.2.0
10
+ - @checkstack/backend-api@0.13.1
11
+ - @checkstack/cache-api@0.2.1
12
+ - @checkstack/queue-api@0.2.15
13
+
14
+ ## 0.7.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 8d1ef12: ## Infrastructure Configuration Shell & Cache System
19
+
20
+ ### New Packages
21
+
22
+ - **`@checkstack/cache-api`**: Core cache abstractions — `CacheProvider` interface, `createScopedCache` factory for plugin key isolation, `CachePlugin`/`CacheManager` lifecycle interfaces.
23
+ - **`@checkstack/cache-common`**: Shared cache types, RPC contract (`getPlugins`, `getConfiguration`, `updateConfiguration`), access rules, and plugin metadata.
24
+ - **`@checkstack/cache-backend`**: Cache settings RPC router — exposes plugin discovery, configuration read/write endpoints with access-gated authorization.
25
+ - **`@checkstack/cache-frontend`**: Cache configuration tab component for the Infrastructure Settings page.
26
+ - **`@checkstack/infrastructure-common`**: Infrastructure tab registry, routes, and shared types for the IDE-style configuration shell.
27
+ - **`@checkstack/infrastructure-frontend`**: Infrastructure Settings page with vertical tab bar, per-tab access control, and user menu integration.
28
+
29
+ ### Modified Packages
30
+
31
+ - **`@checkstack/backend-api`**: Added `cachePluginRegistry` and `cacheManager` to `RpcContext` and `coreServices`.
32
+ - **`@checkstack/backend`**: Registered cache services in boot sequence, added cache config loading, extended dependency sorter for cache plugin ordering.
33
+ - **`@checkstack/queue-frontend`**: Refactored from standalone `/queue/config` route to an infrastructure tab. Queue settings now live inside the Infrastructure Settings page.
34
+
35
+ ### Architecture
36
+
37
+ The former monolithic Queue Config page is replaced by a pluggable Infrastructure Settings shell (`/infrastructure/config`). Plugins register configuration tabs via `registerInfrastructureTab()` with their own access rules, icons, and components. The shell evaluates per-tab access and only renders tabs the user can see.
38
+
39
+ ### Patch Changes
40
+
41
+ - 8d1ef12: ## Per-entity caching with single-flight + safe invalidation across the dashboard hot paths
42
+
43
+ ### `@checkstack/cache-api`
44
+
45
+ - **Breaking** for backend implementors: `CacheProvider` now requires `deleteByPrefix(prefix: string): Promise<number>` for family-level invalidation. The in-memory provider implements it; downstream providers (Redis, etc.) must add it before upgrading.
46
+ - `createScopedCache` forwards `deleteByPrefix` and keeps prefixes scoped to the calling plugin.
47
+
48
+ ### `@checkstack/cache-utils` (new package)
49
+
50
+ High-level read-through caching helpers built on `CacheProvider`:
51
+
52
+ - `createCachedScope({ cacheManager, pluginId })` returns a scope with `wrap`, `wrapMany`, `invalidate`, and `invalidatePrefix`.
53
+ - **Single-flight**: concurrent cache misses for the same key share one loader.
54
+ - **Per-entity bulk caching** via `wrapMany` so list/bulk RPCs cache by id rather than by the full input shape — overlapping callers share entries and invalidation stays exact.
55
+ - **Race-safe invalidation** via per-key epoch counters: a loader started before a mutation cannot repopulate the cache with stale data after the mutation invalidates it. The mutation invariant is `db.write → cache.invalidate (await) → signals.emit`.
56
+ - Cache failures fall through to the loader so a cache outage cannot break reads.
57
+
58
+ ### `@checkstack/backend`
59
+
60
+ - The internal null `CacheProvider` (used when no cache backend is configured) now implements the new `deleteByPrefix` method as a no-op. Patch bump only — no behavior change for existing callers.
61
+
62
+ ### `@checkstack/healthcheck-backend`
63
+
64
+ - `getSystemHealthStatus` and `getBulkSystemHealthStatus` now read through a per-system cache (`healthcheck:status:<systemId>`), eliminating N database queries per dashboard refresh for unchanged systems.
65
+ - Mutation paths (configuration CRUD, system associations, satellite ingest, queue-driven check runs, system/satellite removal hooks) invalidate affected keys before broadcasting their signals so frontend refetches always observe fresh data.
66
+
67
+ ### `@checkstack/incident-backend`
68
+
69
+ - `listIncidents`, `getIncident`, `getIncidentsForSystem`, and `getBulkIncidentsForSystems` now read through a scoped cache:
70
+ - per-incident at `incident:<id>`
71
+ - per-system at `system:<systemId>`
72
+ - per-filter-shape at `list:<stable-stringify(filters)>` for the few list shapes the dashboard polls
73
+ - Mutations (`createIncident`, `updateIncident`, `addUpdate`, `resolveIncident`, `deleteIncident`) invalidate the incident, every affected system, and every cached list before broadcasting `INCIDENT_UPDATED`.
74
+ - The catalog `systemDeleted` cleanup hook drops that system's cached entries.
75
+
76
+ ### `@checkstack/maintenance-backend`
77
+
78
+ - `listMaintenances`, `getMaintenance`, `getMaintenancesForSystem`, and `getBulkMaintenancesForSystems` use the same per-entity / per-system / per-filter-shape pattern as incidents.
79
+ - Mutations (`createMaintenance`, `updateMaintenance`, `addUpdate`, `closeMaintenance`, `deleteMaintenance`) invalidate before broadcasting `MAINTENANCE_UPDATED`.
80
+
81
+ ### `@checkstack/catalog-backend`
82
+
83
+ - Topology reads (`getEntities`, `getSystems`, `getSystem`, `getGroups`, `getSystemGroupIds`) cache under the `entity:` family (25s TTL).
84
+ - Views (`getViews`) and per-system contacts (`getSystemContacts`) cache in their own families.
85
+ - System / group / membership mutations drop the entire `entity:` family (every reader joins the same tables); view and contact mutations drop only their respective scopes.
86
+
87
+ ### `@checkstack/slo-backend`
88
+
89
+ - `listObjectives`, `getObjective`, `getObjectivesForSystem`, and `getBulkObjectivesForSystems` cache results including the expensive `engine.computeStatus` output.
90
+ - Per-entity caching for the bulk handler so dashboards with overlapping system sets share entries.
91
+ - Mutations (`createObjective`, `updateObjective`, `deleteObjective`) invalidate before broadcasting `SLO_STATUS_CHANGED`.
92
+
93
+ ### `@checkstack/anomaly-backend`
94
+
95
+ - New `router-cache.ts` adds a cache scope distinct from the existing detector baseline cache, keyed by stable filter hash.
96
+ - `getAnomalies` and `getAnomalyBaselines` cache through this scope (15s TTL).
97
+ - The detector invalidates the router cache before broadcasting `ANOMALY_STATE_CHANGED` on every state transition (suspicious/anomaly/recovered).
98
+ - Config mutations also invalidate.
99
+
100
+ ### `@checkstack/notification-backend`
101
+
102
+ - `getUnreadCount`, `getNotifications`, and `getSubscriptions` cache per-user.
103
+ - `markAsRead`, `deleteNotification`, `notifyUsers`, and `notifyGroups` invalidate every affected user's cache before sending realtime signals to that user.
104
+ - `subscribe` and `unsubscribe` invalidate the user's subscription cache.
105
+
106
+ ### `@checkstack/announcement-backend`
107
+
108
+ - `getActiveAnnouncements` caches per-user (or anonymous) and per-`includeDismissed` flag (45s TTL — admin-driven, slowly changing).
109
+ - `listAllAnnouncements` caches under a single key.
110
+ - `dismissAnnouncement` only drops that user's cache; `createAnnouncement`, `updateAnnouncement`, `deleteAnnouncement` drop every user's cache before broadcasting `ANNOUNCEMENT_UPDATED`.
111
+ - The auth `userDeleted` cleanup hook drops that user's cached entries.
112
+
113
+ - Updated dependencies [8d1ef12]
114
+ - Updated dependencies [8d1ef12]
115
+ - Updated dependencies [8d1ef12]
116
+ - @checkstack/common@0.7.0
117
+ - @checkstack/cache-api@0.2.0
118
+ - @checkstack/backend-api@0.13.0
119
+ - @checkstack/api-docs-common@0.1.10
120
+ - @checkstack/auth-common@0.6.3
121
+ - @checkstack/signal-backend@0.1.20
122
+ - @checkstack/signal-common@0.1.10
123
+ - @checkstack/queue-api@0.2.14
124
+
3
125
  ## 0.6.6
4
126
 
5
127
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.6.6",
3
+ "version": "0.7.1",
4
4
  "checkstack": {
5
5
  "type": "backend"
6
6
  },
@@ -13,14 +13,15 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/api-docs-common": "0.1.9",
17
- "@checkstack/auth-common": "0.6.1",
18
- "@checkstack/backend-api": "0.12.0",
19
- "@checkstack/common": "0.6.5",
16
+ "@checkstack/api-docs-common": "0.1.10",
17
+ "@checkstack/auth-common": "0.6.3",
18
+ "@checkstack/backend-api": "0.13.0",
19
+ "@checkstack/common": "0.7.0",
20
20
  "@checkstack/drizzle-helper": "0.0.4",
21
- "@checkstack/queue-api": "0.2.13",
22
- "@checkstack/signal-backend": "0.1.19",
23
- "@checkstack/signal-common": "0.1.9",
21
+ "@checkstack/cache-api": "0.2.0",
22
+ "@checkstack/queue-api": "0.2.14",
23
+ "@checkstack/signal-backend": "0.1.20",
24
+ "@checkstack/signal-common": "0.1.10",
24
25
  "@hono/zod-validator": "^0.7.6",
25
26
  "@orpc/client": "^1.13.14",
26
27
  "@orpc/contract": "^1.13.14",
@@ -40,6 +41,6 @@
40
41
  "@types/bun": "latest",
41
42
  "@checkstack/tsconfig": "0.0.5",
42
43
  "@checkstack/scripts": "0.1.2",
43
- "@checkstack/test-utils-backend": "0.1.19"
44
+ "@checkstack/test-utils-backend": "0.1.20"
44
45
  }
45
46
  }
package/src/index.ts CHANGED
@@ -13,6 +13,8 @@ import { eq, and } from "drizzle-orm";
13
13
  import { PluginLocalInstaller } from "./services/plugin-installer";
14
14
  import { QueuePluginRegistryImpl } from "./services/queue-plugin-registry";
15
15
  import { QueueManagerImpl } from "./services/queue-manager";
16
+ import { CachePluginRegistryImpl } from "./services/cache-plugin-registry";
17
+ import { CacheManagerImpl } from "./services/cache-manager";
16
18
  import {
17
19
  createWebSocketHandler,
18
20
  SignalServiceImpl,
@@ -278,6 +280,20 @@ const init = async () => {
278
280
  );
279
281
  pluginManager.registerService(coreServices.queueManager, queueManager);
280
282
 
283
+ // 1.8. Register Cache Services
284
+ rootLogger.debug("Registering cache services...");
285
+ const cacheRegistry = new CachePluginRegistryImpl();
286
+ const cacheManager = new CacheManagerImpl(
287
+ cacheRegistry,
288
+ configService,
289
+ rootLogger
290
+ );
291
+ pluginManager.registerService(
292
+ coreServices.cachePluginRegistry,
293
+ cacheRegistry
294
+ );
295
+ pluginManager.registerService(coreServices.cacheManager, cacheManager);
296
+
281
297
  // Serve static assets for runtime frontend plugins only
282
298
  // Backend plugins don't need public assets - only frontend plugins do
283
299
  // e.g. /assets/plugins/my-plugin-frontend/index.js -> runtime_plugins/node_modules/my-plugin-frontend/dist/index.js
@@ -378,6 +394,10 @@ const init = async () => {
378
394
  // 7. Start config polling for multi-instance coordination
379
395
  queueManager.startPolling(5000);
380
396
 
397
+ // 8. Load Cache Configuration AFTER plugins (cache plugins register first)
398
+ rootLogger.info("📦 Loading cache configuration...");
399
+ await cacheManager.loadConfiguration();
400
+
381
401
  // 9. Setup plugin lifecycle signal broadcasting to frontend
382
402
  // Only broadcast for frontend plugins (plugins ending with -frontend)
383
403
  await eventBus.subscribe(
@@ -13,6 +13,10 @@ import {
13
13
  type Hook,
14
14
  } from "@checkstack/backend-api";
15
15
  import type { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
16
+ import type {
17
+ CachePluginRegistry,
18
+ CacheManager,
19
+ } from "@checkstack/cache-api";
16
20
  import type { ServiceRegistry } from "../services/service-registry";
17
21
  import type { EventBus } from "@checkstack/backend-api";
18
22
  import type { PluginMetadata } from "@checkstack/common";
@@ -98,6 +102,10 @@ export function createApiRouteHandler({
98
102
  coreServices.queuePluginRegistry,
99
103
  );
100
104
  const queueManager = await getService(coreServices.queueManager);
105
+ const cachePluginRegistry = await getService(
106
+ coreServices.cachePluginRegistry,
107
+ );
108
+ const cacheManager = await getService(coreServices.cacheManager);
101
109
  const eventBus = await getService(coreServices.eventBus);
102
110
 
103
111
  if (
@@ -109,6 +117,8 @@ export function createApiRouteHandler({
109
117
  !collectorRegistry ||
110
118
  !queuePluginRegistry ||
111
119
  !queueManager ||
120
+ !cachePluginRegistry ||
121
+ !cacheManager ||
112
122
  !eventBus
113
123
  ) {
114
124
  return c.json({ error: "Core services not initialized" }, 500);
@@ -139,6 +149,8 @@ export function createApiRouteHandler({
139
149
  collectorRegistry: collectorRegistry as CollectorRegistry,
140
150
  queuePluginRegistry: queuePluginRegistry as QueuePluginRegistry,
141
151
  queueManager: queueManager as QueueManager,
152
+ cachePluginRegistry: cachePluginRegistry as CachePluginRegistry,
153
+ cacheManager: cacheManager as CacheManager,
142
154
  user,
143
155
  emitHook,
144
156
  };
@@ -30,11 +30,16 @@ export function sortPlugins({
30
30
 
31
31
  // Track queue plugin providers (plugins that depend on queuePluginRegistry)
32
32
  const queuePluginProviders = new Set<string>();
33
+ // Track cache plugin providers (plugins that depend on cachePluginRegistry)
34
+ const cachePluginProviders = new Set<string>();
33
35
  for (const p of pendingInits) {
34
36
  for (const [, ref] of Object.entries(p.deps)) {
35
37
  if (ref.id === coreServices.queuePluginRegistry.id) {
36
38
  queuePluginProviders.add(p.metadata.pluginId);
37
39
  }
40
+ if (ref.id === coreServices.cachePluginRegistry.id) {
41
+ cachePluginProviders.add(p.metadata.pluginId);
42
+ }
38
43
  }
39
44
  }
40
45
 
@@ -72,6 +77,25 @@ export function sortPlugins({
72
77
  }
73
78
  }
74
79
  }
80
+
81
+ // Special handling: if this plugin uses cacheManager, it must wait for all cache plugin providers
82
+ const usesCacheManager = Object.values(p.deps).some(
83
+ (ref) => ref.id === coreServices.cacheManager.id
84
+ );
85
+ if (usesCacheManager) {
86
+ for (const cpp of cachePluginProviders) {
87
+ if (cpp !== consumerId) {
88
+ if (!graph.has(cpp)) {
89
+ graph.set(cpp, []);
90
+ }
91
+ // Add edge: cache plugin provider -> cache consumer
92
+ if (!graph.get(cpp)!.includes(consumerId)) {
93
+ graph.get(cpp)!.push(consumerId);
94
+ inDegree.set(consumerId, (inDegree.get(consumerId) || 0) + 1);
95
+ }
96
+ }
97
+ }
98
+ }
75
99
  }
76
100
 
77
101
  const queue: string[] = [];
@@ -58,6 +58,13 @@ describe("RPC REST Compatibility", () => {
58
58
  getActivePlugin: () => "none",
59
59
  getQueue: () => ({}),
60
60
  } as any);
61
+ pluginManager.registerService(coreServices.cachePluginRegistry, {
62
+ getPlugins: () => [],
63
+ } as any);
64
+ pluginManager.registerService(coreServices.cacheManager, {
65
+ getActivePlugin: () => "memory",
66
+ getProvider: () => ({}),
67
+ } as any);
61
68
 
62
69
  // 4. Mount the plugins
63
70
  await pluginManager.loadPlugins(app);
@@ -0,0 +1,191 @@
1
+ import type { CacheManager, CacheProvider } from "@checkstack/cache-api";
2
+ import type { CachePluginRegistryImpl } from "./cache-plugin-registry";
3
+ import type { Logger, ConfigService } from "@checkstack/backend-api";
4
+ import { z } from "zod";
5
+ import { extractErrorMessage } from "@checkstack/common";
6
+
7
+ /**
8
+ * Schema for the active cache plugin pointer.
9
+ * Stored in the ConfigService for persistence and multi-instance coordination.
10
+ */
11
+ const activeCachePointerSchema = z.object({
12
+ activePluginId: z.string(),
13
+ version: z.number(),
14
+ });
15
+
16
+ type ActiveCachePointer = z.infer<typeof activeCachePointerSchema>;
17
+
18
+ /**
19
+ * A no-op CacheProvider used before any backend is configured.
20
+ * All operations are safe to call but behave as if the cache is empty.
21
+ */
22
+ const nullProvider: CacheProvider = {
23
+ // eslint-disable-next-line unicorn/no-useless-undefined
24
+ get: async () => undefined,
25
+ set: async () => {},
26
+ delete: async () => {},
27
+ deleteByPrefix: async () => 0,
28
+ has: async () => false,
29
+ };
30
+
31
+ /**
32
+ * CacheManagerImpl handles cache provider lifecycle and backend switching.
33
+ *
34
+ * Simpler than QueueManagerImpl — no proxy pattern needed since cache is
35
+ * stateless key/value. The active provider is replaced atomically on backend switch.
36
+ */
37
+ export class CacheManagerImpl implements CacheManager {
38
+ private activePluginId: string = "memory";
39
+ private activeConfig: unknown = {
40
+ maxEntries: 10_000,
41
+ sweepIntervalMs: 60_000,
42
+ };
43
+ private configVersion: number = 0;
44
+ private activeProvider: CacheProvider = nullProvider;
45
+
46
+ constructor(
47
+ private registry: CachePluginRegistryImpl,
48
+ private configService: ConfigService,
49
+ private logger: Logger,
50
+ ) {}
51
+
52
+ async loadConfiguration(): Promise<void> {
53
+ try {
54
+ const pointer = await this.configService.get<ActiveCachePointer>(
55
+ "cache:active",
56
+ activeCachePointerSchema,
57
+ 1,
58
+ );
59
+
60
+ if (pointer) {
61
+ this.activePluginId = pointer.activePluginId;
62
+ this.configVersion = pointer.version;
63
+
64
+ const plugin = this.registry.getPlugin(this.activePluginId);
65
+ if (plugin) {
66
+ const config = await this.configService.get(
67
+ this.activePluginId,
68
+ plugin.configSchema,
69
+ plugin.configVersion,
70
+ );
71
+
72
+ if (config) {
73
+ this.activeConfig = config;
74
+ }
75
+ }
76
+
77
+ this.logger.info(
78
+ `📦 Loaded cache configuration: plugin=${this.activePluginId}, version=${this.configVersion}`,
79
+ );
80
+ } else {
81
+ this.logger.info(
82
+ `📦 No cache configuration found, using default: plugin=${this.activePluginId}`,
83
+ );
84
+ }
85
+
86
+ // Initialize the active provider
87
+ this.initializeProvider();
88
+ } catch (error) {
89
+ this.logger.error("Failed to load cache configuration", error);
90
+ // Continue with defaults — nullProvider is already set
91
+ }
92
+ }
93
+
94
+ private initializeProvider(): void {
95
+ const plugin = this.registry.getPlugin(this.activePluginId);
96
+ if (!plugin) {
97
+ this.logger.warn(
98
+ `Cache plugin '${this.activePluginId}' not found, using null provider`,
99
+ );
100
+ return;
101
+ }
102
+
103
+ try {
104
+ this.activeProvider = plugin.createProvider(
105
+ this.activeConfig,
106
+ this.logger,
107
+ );
108
+ } catch (error) {
109
+ this.logger.error(
110
+ `Failed to create cache provider for '${this.activePluginId}'`,
111
+ error,
112
+ );
113
+ }
114
+ }
115
+
116
+ getProvider(): CacheProvider {
117
+ return this.activeProvider;
118
+ }
119
+
120
+ getActivePlugin(): string {
121
+ return this.activePluginId;
122
+ }
123
+
124
+ getActiveConfig(): unknown {
125
+ return this.activeConfig;
126
+ }
127
+
128
+ async setActiveBackend(pluginId: string, config: unknown): Promise<void> {
129
+ // 1. Validate plugin exists
130
+ const newPlugin = this.registry.getPlugin(pluginId);
131
+ if (!newPlugin) {
132
+ throw new Error(`Cache plugin '${pluginId}' not found`);
133
+ }
134
+
135
+ // 2. Validate config against schema
136
+ newPlugin.configSchema.parse(config);
137
+
138
+ // 3. Create new provider (acts as connection test)
139
+ this.logger.info("🔍 Testing new cache provider...");
140
+ let newProvider: CacheProvider;
141
+ try {
142
+ newProvider = newPlugin.createProvider(config, this.logger);
143
+ // Quick smoke test
144
+ await newProvider.set("__test__", true, 1000);
145
+ await newProvider.delete("__test__");
146
+ this.logger.info("✅ Cache provider test successful");
147
+ } catch (error) {
148
+ const message = extractErrorMessage(error);
149
+ this.logger.error(`❌ Cache provider test failed: ${message}`);
150
+ throw new Error(`Failed to create cache provider: ${message}`);
151
+ }
152
+
153
+ // 4. Stop old provider if it has a stop method
154
+ const oldProvider = this.activeProvider;
155
+ if ("stop" in oldProvider && typeof oldProvider.stop === "function") {
156
+ await (oldProvider as { stop: () => Promise<void> }).stop();
157
+ }
158
+
159
+ // 5. Switch to new provider
160
+ const oldPluginId = this.activePluginId;
161
+ this.activePluginId = pluginId;
162
+ this.activeConfig = config;
163
+ this.configVersion++;
164
+ this.activeProvider = newProvider;
165
+
166
+ // 6. Persist configuration
167
+ await this.configService.set(
168
+ pluginId,
169
+ newPlugin.configSchema,
170
+ newPlugin.configVersion,
171
+ config,
172
+ );
173
+
174
+ await this.configService.set("cache:active", activeCachePointerSchema, 1, {
175
+ activePluginId: pluginId,
176
+ version: this.configVersion,
177
+ });
178
+
179
+ this.logger.info(`✅ Cache backend switched: ${oldPluginId} → ${pluginId}`);
180
+ }
181
+
182
+ async shutdown(): Promise<void> {
183
+ this.logger.info("🛑 Shutting down cache provider...");
184
+ const provider = this.activeProvider;
185
+ if ("stop" in provider && typeof provider.stop === "function") {
186
+ await (provider as { stop: () => Promise<void> }).stop();
187
+ }
188
+ this.activeProvider = nullProvider;
189
+ this.logger.info("✅ Cache provider shut down");
190
+ }
191
+ }
@@ -0,0 +1,20 @@
1
+ import type {
2
+ CachePlugin,
3
+ CachePluginRegistry,
4
+ } from "@checkstack/cache-api";
5
+
6
+ export class CachePluginRegistryImpl implements CachePluginRegistry {
7
+ private plugins = new Map<string, CachePlugin<unknown>>();
8
+
9
+ register(plugin: CachePlugin<unknown>): void {
10
+ this.plugins.set(plugin.id, plugin);
11
+ }
12
+
13
+ getPlugin(id: string): CachePlugin<unknown> | undefined {
14
+ return this.plugins.get(id);
15
+ }
16
+
17
+ getPlugins(): CachePlugin<unknown>[] {
18
+ return [...this.plugins.values()];
19
+ }
20
+ }