@checkstack/backend 0.6.6 → 0.7.0
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 +111 -0
- package/package.json +3 -2
- package/src/index.ts +20 -0
- package/src/plugin-manager/api-router.ts +12 -0
- package/src/plugin-manager/dependency-sorter.ts +24 -0
- package/src/rpc-rest-compat.test.ts +7 -0
- package/src/services/cache-manager.ts +191 -0
- package/src/services/cache-plugin-registry.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,116 @@
|
|
|
1
1
|
# @checkstack/backend
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8d1ef12: ## Infrastructure Configuration Shell & Cache System
|
|
8
|
+
|
|
9
|
+
### New Packages
|
|
10
|
+
|
|
11
|
+
- **`@checkstack/cache-api`**: Core cache abstractions — `CacheProvider` interface, `createScopedCache` factory for plugin key isolation, `CachePlugin`/`CacheManager` lifecycle interfaces.
|
|
12
|
+
- **`@checkstack/cache-common`**: Shared cache types, RPC contract (`getPlugins`, `getConfiguration`, `updateConfiguration`), access rules, and plugin metadata.
|
|
13
|
+
- **`@checkstack/cache-backend`**: Cache settings RPC router — exposes plugin discovery, configuration read/write endpoints with access-gated authorization.
|
|
14
|
+
- **`@checkstack/cache-frontend`**: Cache configuration tab component for the Infrastructure Settings page.
|
|
15
|
+
- **`@checkstack/infrastructure-common`**: Infrastructure tab registry, routes, and shared types for the IDE-style configuration shell.
|
|
16
|
+
- **`@checkstack/infrastructure-frontend`**: Infrastructure Settings page with vertical tab bar, per-tab access control, and user menu integration.
|
|
17
|
+
|
|
18
|
+
### Modified Packages
|
|
19
|
+
|
|
20
|
+
- **`@checkstack/backend-api`**: Added `cachePluginRegistry` and `cacheManager` to `RpcContext` and `coreServices`.
|
|
21
|
+
- **`@checkstack/backend`**: Registered cache services in boot sequence, added cache config loading, extended dependency sorter for cache plugin ordering.
|
|
22
|
+
- **`@checkstack/queue-frontend`**: Refactored from standalone `/queue/config` route to an infrastructure tab. Queue settings now live inside the Infrastructure Settings page.
|
|
23
|
+
|
|
24
|
+
### Architecture
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 8d1ef12: ## Per-entity caching with single-flight + safe invalidation across the dashboard hot paths
|
|
31
|
+
|
|
32
|
+
### `@checkstack/cache-api`
|
|
33
|
+
|
|
34
|
+
- **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.
|
|
35
|
+
- `createScopedCache` forwards `deleteByPrefix` and keeps prefixes scoped to the calling plugin.
|
|
36
|
+
|
|
37
|
+
### `@checkstack/cache-utils` (new package)
|
|
38
|
+
|
|
39
|
+
High-level read-through caching helpers built on `CacheProvider`:
|
|
40
|
+
|
|
41
|
+
- `createCachedScope({ cacheManager, pluginId })` returns a scope with `wrap`, `wrapMany`, `invalidate`, and `invalidatePrefix`.
|
|
42
|
+
- **Single-flight**: concurrent cache misses for the same key share one loader.
|
|
43
|
+
- **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.
|
|
44
|
+
- **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`.
|
|
45
|
+
- Cache failures fall through to the loader so a cache outage cannot break reads.
|
|
46
|
+
|
|
47
|
+
### `@checkstack/backend`
|
|
48
|
+
|
|
49
|
+
- 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.
|
|
50
|
+
|
|
51
|
+
### `@checkstack/healthcheck-backend`
|
|
52
|
+
|
|
53
|
+
- `getSystemHealthStatus` and `getBulkSystemHealthStatus` now read through a per-system cache (`healthcheck:status:<systemId>`), eliminating N database queries per dashboard refresh for unchanged systems.
|
|
54
|
+
- 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.
|
|
55
|
+
|
|
56
|
+
### `@checkstack/incident-backend`
|
|
57
|
+
|
|
58
|
+
- `listIncidents`, `getIncident`, `getIncidentsForSystem`, and `getBulkIncidentsForSystems` now read through a scoped cache:
|
|
59
|
+
- per-incident at `incident:<id>`
|
|
60
|
+
- per-system at `system:<systemId>`
|
|
61
|
+
- per-filter-shape at `list:<stable-stringify(filters)>` for the few list shapes the dashboard polls
|
|
62
|
+
- Mutations (`createIncident`, `updateIncident`, `addUpdate`, `resolveIncident`, `deleteIncident`) invalidate the incident, every affected system, and every cached list before broadcasting `INCIDENT_UPDATED`.
|
|
63
|
+
- The catalog `systemDeleted` cleanup hook drops that system's cached entries.
|
|
64
|
+
|
|
65
|
+
### `@checkstack/maintenance-backend`
|
|
66
|
+
|
|
67
|
+
- `listMaintenances`, `getMaintenance`, `getMaintenancesForSystem`, and `getBulkMaintenancesForSystems` use the same per-entity / per-system / per-filter-shape pattern as incidents.
|
|
68
|
+
- Mutations (`createMaintenance`, `updateMaintenance`, `addUpdate`, `closeMaintenance`, `deleteMaintenance`) invalidate before broadcasting `MAINTENANCE_UPDATED`.
|
|
69
|
+
|
|
70
|
+
### `@checkstack/catalog-backend`
|
|
71
|
+
|
|
72
|
+
- Topology reads (`getEntities`, `getSystems`, `getSystem`, `getGroups`, `getSystemGroupIds`) cache under the `entity:` family (25s TTL).
|
|
73
|
+
- Views (`getViews`) and per-system contacts (`getSystemContacts`) cache in their own families.
|
|
74
|
+
- System / group / membership mutations drop the entire `entity:` family (every reader joins the same tables); view and contact mutations drop only their respective scopes.
|
|
75
|
+
|
|
76
|
+
### `@checkstack/slo-backend`
|
|
77
|
+
|
|
78
|
+
- `listObjectives`, `getObjective`, `getObjectivesForSystem`, and `getBulkObjectivesForSystems` cache results including the expensive `engine.computeStatus` output.
|
|
79
|
+
- Per-entity caching for the bulk handler so dashboards with overlapping system sets share entries.
|
|
80
|
+
- Mutations (`createObjective`, `updateObjective`, `deleteObjective`) invalidate before broadcasting `SLO_STATUS_CHANGED`.
|
|
81
|
+
|
|
82
|
+
### `@checkstack/anomaly-backend`
|
|
83
|
+
|
|
84
|
+
- New `router-cache.ts` adds a cache scope distinct from the existing detector baseline cache, keyed by stable filter hash.
|
|
85
|
+
- `getAnomalies` and `getAnomalyBaselines` cache through this scope (15s TTL).
|
|
86
|
+
- The detector invalidates the router cache before broadcasting `ANOMALY_STATE_CHANGED` on every state transition (suspicious/anomaly/recovered).
|
|
87
|
+
- Config mutations also invalidate.
|
|
88
|
+
|
|
89
|
+
### `@checkstack/notification-backend`
|
|
90
|
+
|
|
91
|
+
- `getUnreadCount`, `getNotifications`, and `getSubscriptions` cache per-user.
|
|
92
|
+
- `markAsRead`, `deleteNotification`, `notifyUsers`, and `notifyGroups` invalidate every affected user's cache before sending realtime signals to that user.
|
|
93
|
+
- `subscribe` and `unsubscribe` invalidate the user's subscription cache.
|
|
94
|
+
|
|
95
|
+
### `@checkstack/announcement-backend`
|
|
96
|
+
|
|
97
|
+
- `getActiveAnnouncements` caches per-user (or anonymous) and per-`includeDismissed` flag (45s TTL — admin-driven, slowly changing).
|
|
98
|
+
- `listAllAnnouncements` caches under a single key.
|
|
99
|
+
- `dismissAnnouncement` only drops that user's cache; `createAnnouncement`, `updateAnnouncement`, `deleteAnnouncement` drop every user's cache before broadcasting `ANNOUNCEMENT_UPDATED`.
|
|
100
|
+
- The auth `userDeleted` cleanup hook drops that user's cached entries.
|
|
101
|
+
|
|
102
|
+
- Updated dependencies [8d1ef12]
|
|
103
|
+
- Updated dependencies [8d1ef12]
|
|
104
|
+
- Updated dependencies [8d1ef12]
|
|
105
|
+
- @checkstack/common@0.7.0
|
|
106
|
+
- @checkstack/cache-api@0.2.0
|
|
107
|
+
- @checkstack/backend-api@0.13.0
|
|
108
|
+
- @checkstack/api-docs-common@0.1.10
|
|
109
|
+
- @checkstack/auth-common@0.6.3
|
|
110
|
+
- @checkstack/signal-backend@0.1.20
|
|
111
|
+
- @checkstack/signal-common@0.1.10
|
|
112
|
+
- @checkstack/queue-api@0.2.14
|
|
113
|
+
|
|
3
114
|
## 0.6.6
|
|
4
115
|
|
|
5
116
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"checkstack": {
|
|
5
5
|
"type": "backend"
|
|
6
6
|
},
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@checkstack/api-docs-common": "0.1.9",
|
|
17
|
-
"@checkstack/auth-common": "0.6.
|
|
17
|
+
"@checkstack/auth-common": "0.6.2",
|
|
18
18
|
"@checkstack/backend-api": "0.12.0",
|
|
19
19
|
"@checkstack/common": "0.6.5",
|
|
20
20
|
"@checkstack/drizzle-helper": "0.0.4",
|
|
21
|
+
"@checkstack/cache-api": "0.1.0",
|
|
21
22
|
"@checkstack/queue-api": "0.2.13",
|
|
22
23
|
"@checkstack/signal-backend": "0.1.19",
|
|
23
24
|
"@checkstack/signal-common": "0.1.9",
|
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
|
+
}
|