@frontmcp/plugin-cache 0.0.1 → 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/cache.plugin.d.ts +29 -0
- package/cache.plugin.d.ts.map +1 -0
- package/cache.symbol.d.ts +4 -0
- package/cache.symbol.d.ts.map +1 -0
- package/cache.types.d.ts +104 -0
- package/cache.types.d.ts.map +1 -0
- package/esm/index.mjs +474 -0
- package/esm/package.json +63 -0
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -0
- package/index.js +494 -0
- package/package.json +2 -2
- package/providers/cache-memory.provider.d.ts +20 -0
- package/providers/cache-memory.provider.d.ts.map +1 -0
- package/providers/cache-redis.provider.d.ts +16 -0
- package/providers/cache-redis.provider.d.ts.map +1 -0
- package/providers/cache-vercel-kv.provider.d.ts +25 -0
- package/providers/cache-vercel-kv.provider.d.ts.map +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DynamicPlugin, FlowCtxOf, ProviderType } from '@frontmcp/sdk';
|
|
2
|
+
import { CachePluginOptions } from './cache.types';
|
|
3
|
+
export default class CachePlugin extends DynamicPlugin<CachePluginOptions> {
|
|
4
|
+
static dynamicProviders: (options: CachePluginOptions) => ProviderType[];
|
|
5
|
+
static defaultOptions: CachePluginOptions;
|
|
6
|
+
options: CachePluginOptions;
|
|
7
|
+
constructor(options?: CachePluginOptions);
|
|
8
|
+
/**
|
|
9
|
+
* Check if a tool should be cached based on metadata or tools list.
|
|
10
|
+
*/
|
|
11
|
+
private shouldCacheTool;
|
|
12
|
+
/**
|
|
13
|
+
* Check if cache should be bypassed based on request headers.
|
|
14
|
+
* Accesses headers from FrontMcpContextStorage which extracts x-frontmcp-* custom headers.
|
|
15
|
+
*/
|
|
16
|
+
private shouldBypassCache;
|
|
17
|
+
/**
|
|
18
|
+
* Get TTL for a tool, with metadata taking precedence over defaults.
|
|
19
|
+
*/
|
|
20
|
+
private getTtl;
|
|
21
|
+
willReadCache(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void>;
|
|
22
|
+
willWriteCache(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if a tool is cacheable based on metadata or tools list.
|
|
25
|
+
* This can be used by other plugins or flows to determine cacheability.
|
|
26
|
+
*/
|
|
27
|
+
isCacheable(toolName: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=cache.plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.plugin.d.ts","sourceRoot":"","sources":["../src/cache.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,SAAS,EAET,YAAY,EAOb,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,kBAAkB,EAAiC,MAAM,eAAe,CAAC;AAyClF,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,aAAa,CAAC,kBAAkB,CAAC;IACxE,OAAgB,gBAAgB,GAAI,SAAS,kBAAkB,oBAqD7D;IAEF,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAEvC;IACF,OAAO,EAAE,kBAAkB,CAAC;gBAEhB,OAAO,GAAE,kBAA+C;IAQpE;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,OAAO,CAAC,MAAM;IAQR,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IA0EnD,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IAyB1D;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;CAGvC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.symbol.d.ts","sourceRoot":"","sources":["../src/cache.symbol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,mBAAmB,CAAgC,CAAC"}
|
package/cache.types.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Redis as RedisClient } from 'ioredis';
|
|
2
|
+
declare global {
|
|
3
|
+
interface ExtendFrontMcpToolMetadata {
|
|
4
|
+
cache?: CachePluginToolOptions | true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export interface CachePluginToolOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Time to live in seconds. Default is 1 day.
|
|
10
|
+
*/
|
|
11
|
+
ttl?: number;
|
|
12
|
+
/**
|
|
13
|
+
* If true, the cache value will be updated with the new value after the TTL.
|
|
14
|
+
* Default is false.
|
|
15
|
+
*/
|
|
16
|
+
slideWindow?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface BaseCachePluginOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Default time to live in seconds. Default is 1 day (86400 seconds).
|
|
21
|
+
*/
|
|
22
|
+
defaultTTL?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Tool names or glob patterns to cache.
|
|
25
|
+
* Supports exact names ('tool-name') and wildcards ('namespace:*').
|
|
26
|
+
* Tools matching these patterns use defaultTTL unless they have custom cache metadata.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* toolPatterns: ['mintlify:*', 'local:ping', 'api:get-*']
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
toolPatterns?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* HTTP header name that clients can send to bypass cache for a specific request.
|
|
36
|
+
* When this header is present with a truthy value ('true' or '1'), cache read/write is skipped.
|
|
37
|
+
*
|
|
38
|
+
* Note: Headers must use the `x-frontmcp-*` prefix to be captured by the context storage.
|
|
39
|
+
* Custom bypass headers should also follow this convention.
|
|
40
|
+
*
|
|
41
|
+
* @default 'x-frontmcp-disable-cache'
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // Plugin config with custom bypass header
|
|
46
|
+
* CachePlugin.init({ type: 'memory', bypassHeader: 'x-frontmcp-no-cache' })
|
|
47
|
+
*
|
|
48
|
+
* // Client request with header
|
|
49
|
+
* fetch('/mcp', { headers: { 'x-frontmcp-disable-cache': 'true' } })
|
|
50
|
+
* // or
|
|
51
|
+
* fetch('/mcp', { headers: { 'x-frontmcp-disable-cache': '1' } })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
bypassHeader?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface RedisClientCachePluginOptions extends BaseCachePluginOptions {
|
|
57
|
+
type: 'redis-client';
|
|
58
|
+
client: RedisClient;
|
|
59
|
+
}
|
|
60
|
+
export interface RedisCachePluginOptions extends BaseCachePluginOptions {
|
|
61
|
+
type: 'redis';
|
|
62
|
+
config: {
|
|
63
|
+
host: string;
|
|
64
|
+
port: number;
|
|
65
|
+
password?: string;
|
|
66
|
+
db?: number;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export type MemoryCachePluginOptions = BaseCachePluginOptions & {
|
|
70
|
+
type: 'memory';
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Use global store configuration from @FrontMcp decorator.
|
|
74
|
+
* Requires `redis` to be configured in the main FrontMcp options.
|
|
75
|
+
* Supports both Redis and Vercel KV global configurations.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* // In main.ts - configure global store
|
|
80
|
+
* @FrontMcp({
|
|
81
|
+
* redis: { host: 'localhost', port: 6379 },
|
|
82
|
+
* apps: [MyApp]
|
|
83
|
+
* })
|
|
84
|
+
*
|
|
85
|
+
* // In app - use global store
|
|
86
|
+
* @App({
|
|
87
|
+
* plugins: [CachePlugin.init({ type: 'global-store' })]
|
|
88
|
+
* })
|
|
89
|
+
* class MyApp {}
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export interface GlobalStoreCachePluginOptions extends BaseCachePluginOptions {
|
|
93
|
+
type: 'global-store';
|
|
94
|
+
}
|
|
95
|
+
export type RedisCacheOptions = RedisClientCachePluginOptions | RedisCachePluginOptions;
|
|
96
|
+
export type CachePluginOptions = MemoryCachePluginOptions | RedisCacheOptions | GlobalStoreCachePluginOptions;
|
|
97
|
+
export interface CacheStoreInterface {
|
|
98
|
+
setValue(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
99
|
+
getValue<T = unknown>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
100
|
+
delete(key: string): Promise<void>;
|
|
101
|
+
exists(key: string): Promise<boolean>;
|
|
102
|
+
close(): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=cache.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.types.d.ts","sourceRoot":"","sources":["../src/cache.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,0BAA0B;QAClC,KAAK,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;KACvC;CACF;AAED,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA8B,SAAQ,sBAAsB;IAC3E,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,uBAAwB,SAAQ,sBAAsB;IACrE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC;CACH;AAED,MAAM,MAAM,wBAAwB,GAAG,sBAAsB,GAAG;IAC9D,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,6BAA8B,SAAQ,sBAAsB;IAC3E,IAAI,EAAE,cAAc,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GAAG,6BAA6B,GAAG,uBAAuB,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG,iBAAiB,GAAG,6BAA6B,CAAC;AAE9G,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1E,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAE7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
11
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
12
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
13
|
+
if (decorator = decorators[i])
|
|
14
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
15
|
+
if (kind && result) __defProp(target, key, result);
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
19
|
+
|
|
20
|
+
// plugins/plugin-cache/src/cache.plugin.ts
|
|
21
|
+
import {
|
|
22
|
+
DynamicPlugin,
|
|
23
|
+
Plugin,
|
|
24
|
+
ToolHook,
|
|
25
|
+
FrontMcpConfig,
|
|
26
|
+
getGlobalStoreConfig,
|
|
27
|
+
isVercelKvProvider,
|
|
28
|
+
FrontMcpContextStorage
|
|
29
|
+
} from "@frontmcp/sdk";
|
|
30
|
+
|
|
31
|
+
// plugins/plugin-cache/src/providers/cache-redis.provider.ts
|
|
32
|
+
import Redis from "ioredis";
|
|
33
|
+
import { Provider, ProviderScope } from "@frontmcp/sdk";
|
|
34
|
+
var CacheRedisProvider = class {
|
|
35
|
+
client;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
if (options.type !== "redis" && options.type !== "redis-client") {
|
|
38
|
+
throw new Error("Invalid cache provider type");
|
|
39
|
+
}
|
|
40
|
+
if (options.type === "redis-client") {
|
|
41
|
+
this.client = options.client;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.client = new Redis({
|
|
45
|
+
lazyConnect: false,
|
|
46
|
+
maxRetriesPerRequest: 3,
|
|
47
|
+
...options.config
|
|
48
|
+
});
|
|
49
|
+
this.client.on("connect", () => console.log("[Redis] Connected"));
|
|
50
|
+
this.client.on("error", (err) => console.error("[Redis] Error:", err));
|
|
51
|
+
}
|
|
52
|
+
/** Set a value (auto-stringifies objects) */
|
|
53
|
+
async setValue(key, value, ttlSeconds) {
|
|
54
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
55
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
56
|
+
await this.client.set(key, strValue, "EX", ttlSeconds);
|
|
57
|
+
} else {
|
|
58
|
+
await this.client.set(key, strValue);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Get a value and automatically parse JSON if possible */
|
|
62
|
+
async getValue(key, defaultValue) {
|
|
63
|
+
const raw = await this.client.get(key);
|
|
64
|
+
if (raw === null) return defaultValue;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Delete a key */
|
|
72
|
+
async delete(key) {
|
|
73
|
+
await this.client.del(key);
|
|
74
|
+
}
|
|
75
|
+
/** Check if a key exists */
|
|
76
|
+
async exists(key) {
|
|
77
|
+
return await this.client.exists(key) === 1;
|
|
78
|
+
}
|
|
79
|
+
/** Gracefully close the Redis connection */
|
|
80
|
+
async close() {
|
|
81
|
+
await this.client.quit();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
CacheRedisProvider = __decorateClass([
|
|
85
|
+
Provider({
|
|
86
|
+
name: "provider:cache:redis",
|
|
87
|
+
description: "Redis-based cache provider",
|
|
88
|
+
scope: ProviderScope.GLOBAL
|
|
89
|
+
})
|
|
90
|
+
], CacheRedisProvider);
|
|
91
|
+
|
|
92
|
+
// plugins/plugin-cache/src/providers/cache-memory.provider.ts
|
|
93
|
+
import { Provider as Provider2, ProviderScope as ProviderScope2 } from "@frontmcp/sdk";
|
|
94
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
95
|
+
var CacheMemoryProvider = class {
|
|
96
|
+
memory = /* @__PURE__ */ new Map();
|
|
97
|
+
sweeper;
|
|
98
|
+
constructor(sweepIntervalTTL = 60) {
|
|
99
|
+
this.sweeper = setInterval(() => this.sweep(), sweepIntervalTTL * 1e3);
|
|
100
|
+
this.sweeper.unref?.();
|
|
101
|
+
}
|
|
102
|
+
/** Set a value (auto-stringifies objects) */
|
|
103
|
+
async setValue(key, value, ttlSeconds) {
|
|
104
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
105
|
+
const existing = this.memory.get(key);
|
|
106
|
+
if (existing?.timeout) clearTimeout(existing.timeout);
|
|
107
|
+
const entry = { value: strValue };
|
|
108
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
109
|
+
const ttlMs = ttlSeconds * 1e3;
|
|
110
|
+
entry.expiresAt = Date.now() + ttlMs;
|
|
111
|
+
if (ttlMs <= MAX_TIMEOUT_MS) {
|
|
112
|
+
entry.timeout = setTimeout(() => {
|
|
113
|
+
const e = this.memory.get(key);
|
|
114
|
+
if (e && e.expiresAt && e.expiresAt <= Date.now()) {
|
|
115
|
+
this.memory.delete(key);
|
|
116
|
+
}
|
|
117
|
+
}, ttlMs);
|
|
118
|
+
entry.timeout.unref?.();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.memory.set(key, entry);
|
|
122
|
+
}
|
|
123
|
+
/** Get a value and automatically parse JSON if possible */
|
|
124
|
+
async getValue(key, defaultValue) {
|
|
125
|
+
const entry = this.memory.get(key);
|
|
126
|
+
if (!entry) return defaultValue;
|
|
127
|
+
if (this.isExpired(entry)) {
|
|
128
|
+
await this.delete(key);
|
|
129
|
+
return defaultValue;
|
|
130
|
+
}
|
|
131
|
+
const raw = entry.value;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
return raw;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Delete a key */
|
|
139
|
+
async delete(key) {
|
|
140
|
+
const entry = this.memory.get(key);
|
|
141
|
+
if (entry?.timeout) clearTimeout(entry.timeout);
|
|
142
|
+
this.memory.delete(key);
|
|
143
|
+
}
|
|
144
|
+
/** Check if a key exists (and not expired) */
|
|
145
|
+
async exists(key) {
|
|
146
|
+
const entry = this.memory.get(key);
|
|
147
|
+
if (!entry) return false;
|
|
148
|
+
if (this.isExpired(entry)) {
|
|
149
|
+
await this.delete(key);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/** Gracefully close the provider */
|
|
155
|
+
async close() {
|
|
156
|
+
if (this.sweeper) clearInterval(this.sweeper);
|
|
157
|
+
for (const [, entry] of this.memory) {
|
|
158
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
159
|
+
}
|
|
160
|
+
this.memory.clear();
|
|
161
|
+
}
|
|
162
|
+
// ---- internals ----
|
|
163
|
+
isExpired(entry) {
|
|
164
|
+
return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
|
|
165
|
+
}
|
|
166
|
+
/** Periodically remove expired keys to keep memory tidy */
|
|
167
|
+
sweep() {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
for (const [key, entry] of this.memory) {
|
|
170
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
|
|
171
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
172
|
+
this.memory.delete(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
CacheMemoryProvider = __decorateClass([
|
|
178
|
+
Provider2({
|
|
179
|
+
name: "provider:cache:memory",
|
|
180
|
+
description: "Memory-based cache provider",
|
|
181
|
+
scope: ProviderScope2.GLOBAL
|
|
182
|
+
})
|
|
183
|
+
], CacheMemoryProvider);
|
|
184
|
+
|
|
185
|
+
// plugins/plugin-cache/src/providers/cache-vercel-kv.provider.ts
|
|
186
|
+
import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
|
|
187
|
+
var CacheVercelKvProvider = class {
|
|
188
|
+
kv;
|
|
189
|
+
keyPrefix;
|
|
190
|
+
defaultTTL;
|
|
191
|
+
constructor(options = {}) {
|
|
192
|
+
const vercelKv = __require("@vercel/kv");
|
|
193
|
+
const hasUrl = options.url !== void 0;
|
|
194
|
+
const hasToken = options.token !== void 0;
|
|
195
|
+
if (hasUrl !== hasToken) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`CacheVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl ? "provided" : "missing"}, token=${hasToken ? "provided" : "missing"}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (options.url && options.token) {
|
|
201
|
+
this.kv = vercelKv.createClient({
|
|
202
|
+
url: options.url,
|
|
203
|
+
token: options.token
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
this.kv = vercelKv.kv;
|
|
207
|
+
}
|
|
208
|
+
this.keyPrefix = options.keyPrefix ?? "cache:";
|
|
209
|
+
this.defaultTTL = options.defaultTTL ?? 60 * 60 * 24;
|
|
210
|
+
}
|
|
211
|
+
prefixKey(key) {
|
|
212
|
+
return `${this.keyPrefix}${key}`;
|
|
213
|
+
}
|
|
214
|
+
/** Set a value (auto-stringifies objects) */
|
|
215
|
+
async setValue(key, value, ttlSeconds) {
|
|
216
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
217
|
+
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
218
|
+
if (ttl > 0) {
|
|
219
|
+
await this.kv.set(this.prefixKey(key), strValue, { ex: ttl });
|
|
220
|
+
} else {
|
|
221
|
+
await this.kv.set(this.prefixKey(key), strValue);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Get a value and automatically parse JSON if possible */
|
|
225
|
+
async getValue(key, defaultValue) {
|
|
226
|
+
const raw = await this.kv.get(this.prefixKey(key));
|
|
227
|
+
if (raw === null || raw === void 0) return defaultValue;
|
|
228
|
+
if (typeof raw === "string") {
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(raw);
|
|
231
|
+
} catch {
|
|
232
|
+
return raw;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return raw;
|
|
236
|
+
}
|
|
237
|
+
/** Delete a key */
|
|
238
|
+
async delete(key) {
|
|
239
|
+
await this.kv.del(this.prefixKey(key));
|
|
240
|
+
}
|
|
241
|
+
/** Check if a key exists */
|
|
242
|
+
async exists(key) {
|
|
243
|
+
return await this.kv.exists(this.prefixKey(key)) === 1;
|
|
244
|
+
}
|
|
245
|
+
/** Gracefully close the provider (no-op for Vercel KV - stateless REST API) */
|
|
246
|
+
async close() {
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
CacheVercelKvProvider = __decorateClass([
|
|
250
|
+
Provider3({
|
|
251
|
+
name: "provider:cache:vercel-kv",
|
|
252
|
+
description: "Vercel KV-based cache provider",
|
|
253
|
+
scope: ProviderScope3.GLOBAL
|
|
254
|
+
})
|
|
255
|
+
], CacheVercelKvProvider);
|
|
256
|
+
|
|
257
|
+
// plugins/plugin-cache/src/cache.symbol.ts
|
|
258
|
+
var CacheStoreToken = /* @__PURE__ */ Symbol("plugin:cache:store");
|
|
259
|
+
|
|
260
|
+
// plugins/plugin-cache/src/cache.plugin.ts
|
|
261
|
+
var DEFAULT_BYPASS_HEADER = "x-frontmcp-disable-cache";
|
|
262
|
+
function matchesToolPattern(toolName, patterns) {
|
|
263
|
+
if (!patterns || patterns.length === 0) return false;
|
|
264
|
+
return patterns.some((pattern) => {
|
|
265
|
+
if (pattern.includes("*")) {
|
|
266
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
267
|
+
const regex = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
|
|
268
|
+
return regex.test(toolName);
|
|
269
|
+
}
|
|
270
|
+
return pattern === toolName;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
var CachePlugin = class extends DynamicPlugin {
|
|
274
|
+
options;
|
|
275
|
+
constructor(options = CachePlugin.defaultOptions) {
|
|
276
|
+
super();
|
|
277
|
+
this.options = {
|
|
278
|
+
defaultTTL: 60 * 60 * 24,
|
|
279
|
+
...options
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if a tool should be cached based on metadata or tools list.
|
|
284
|
+
*/
|
|
285
|
+
shouldCacheTool(toolName, cacheMetadata) {
|
|
286
|
+
if (cacheMetadata) return true;
|
|
287
|
+
const patterns = this.options.toolPatterns ?? [];
|
|
288
|
+
return matchesToolPattern(toolName, patterns);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if cache should be bypassed based on request headers.
|
|
292
|
+
* Accesses headers from FrontMcpContextStorage which extracts x-frontmcp-* custom headers.
|
|
293
|
+
*/
|
|
294
|
+
shouldBypassCache(_flowCtx) {
|
|
295
|
+
const bypassHeader = this.options.bypassHeader ?? DEFAULT_BYPASS_HEADER;
|
|
296
|
+
try {
|
|
297
|
+
const contextStorage = this.get(FrontMcpContextStorage);
|
|
298
|
+
const context = contextStorage?.getStore();
|
|
299
|
+
const customHeaders = context?.metadata?.customHeaders;
|
|
300
|
+
if (!customHeaders) return false;
|
|
301
|
+
const headerKey = bypassHeader.toLowerCase();
|
|
302
|
+
const headerValue = customHeaders[headerKey];
|
|
303
|
+
return headerValue === "true" || headerValue === "1";
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get TTL for a tool, with metadata taking precedence over defaults.
|
|
310
|
+
*/
|
|
311
|
+
getTtl(cacheMetadata) {
|
|
312
|
+
if (typeof cacheMetadata === "object" && cacheMetadata.ttl !== void 0) {
|
|
313
|
+
return cacheMetadata.ttl;
|
|
314
|
+
}
|
|
315
|
+
return this.options.defaultTTL ?? 60 * 60 * 24;
|
|
316
|
+
}
|
|
317
|
+
async willReadCache(flowCtx) {
|
|
318
|
+
const { tool, toolContext } = flowCtx.state;
|
|
319
|
+
if (!tool || !toolContext) return;
|
|
320
|
+
if (this.shouldBypassCache(flowCtx)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const { cache } = toolContext.metadata;
|
|
324
|
+
if (!this.shouldCacheTool(tool.fullName, cache) && !this.shouldCacheTool(tool.name, cache) || typeof toolContext.input === "undefined") {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const cacheStore = this.get(CacheStoreToken);
|
|
328
|
+
const hash = hashObject({ tool: tool.fullName, input: toolContext.input });
|
|
329
|
+
const cached = await cacheStore.getValue(hash);
|
|
330
|
+
if (cached !== void 0 && cached !== null) {
|
|
331
|
+
const cacheConfig = typeof cache === "object" ? cache : void 0;
|
|
332
|
+
if (cache === true || cacheConfig?.ttl && cacheConfig?.slideWindow) {
|
|
333
|
+
const ttl = this.getTtl(cache);
|
|
334
|
+
await cacheStore.setValue(hash, cached, ttl);
|
|
335
|
+
}
|
|
336
|
+
if (!tool.safeParseOutput(cached).success) {
|
|
337
|
+
await cacheStore.delete(hash);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const isPlainObject = typeof cached === "object" && cached !== null && !Array.isArray(cached);
|
|
341
|
+
let cachedWithMeta;
|
|
342
|
+
if (isPlainObject) {
|
|
343
|
+
const cachedRecord = cached;
|
|
344
|
+
const existingMeta = cachedRecord["_meta"] || {};
|
|
345
|
+
cachedWithMeta = {
|
|
346
|
+
...cachedRecord,
|
|
347
|
+
_meta: {
|
|
348
|
+
...existingMeta,
|
|
349
|
+
cache: "hit"
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
} else {
|
|
353
|
+
cachedWithMeta = cached;
|
|
354
|
+
}
|
|
355
|
+
flowCtx.state.rawOutput = cachedWithMeta;
|
|
356
|
+
toolContext.respond(cachedWithMeta);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async willWriteCache(flowCtx) {
|
|
360
|
+
const { tool, toolContext } = flowCtx.state;
|
|
361
|
+
if (!tool || !toolContext) return;
|
|
362
|
+
if (this.shouldBypassCache(flowCtx)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const { cache } = toolContext.metadata;
|
|
366
|
+
const shouldCache = this.shouldCacheTool(tool.fullName, cache) || this.shouldCacheTool(tool.name, cache);
|
|
367
|
+
if (!shouldCache || typeof toolContext.input === "undefined") {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const cacheStore = this.get(CacheStoreToken);
|
|
371
|
+
const ttl = this.getTtl(cache);
|
|
372
|
+
const hash = hashObject({ tool: tool.fullName, input: toolContext.input });
|
|
373
|
+
await cacheStore.setValue(hash, toolContext.output, ttl);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Check if a tool is cacheable based on metadata or tools list.
|
|
377
|
+
* This can be used by other plugins or flows to determine cacheability.
|
|
378
|
+
*/
|
|
379
|
+
isCacheable(toolName) {
|
|
380
|
+
return matchesToolPattern(toolName, this.options.toolPatterns ?? []);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
__publicField(CachePlugin, "dynamicProviders", (options) => {
|
|
384
|
+
const providers = [];
|
|
385
|
+
switch (options.type) {
|
|
386
|
+
case "global-store":
|
|
387
|
+
providers.push({
|
|
388
|
+
name: "cache:global-store",
|
|
389
|
+
provide: CacheStoreToken,
|
|
390
|
+
inject: () => [FrontMcpConfig],
|
|
391
|
+
useFactory: (config) => {
|
|
392
|
+
const storeConfig = getGlobalStoreConfig("CachePlugin", config);
|
|
393
|
+
const globalOptions = options;
|
|
394
|
+
if (isVercelKvProvider(storeConfig)) {
|
|
395
|
+
return new CacheVercelKvProvider({
|
|
396
|
+
url: storeConfig.url,
|
|
397
|
+
token: storeConfig.token,
|
|
398
|
+
keyPrefix: storeConfig.keyPrefix,
|
|
399
|
+
defaultTTL: globalOptions.defaultTTL
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return new CacheRedisProvider({
|
|
403
|
+
type: "redis",
|
|
404
|
+
config: {
|
|
405
|
+
host: storeConfig.host ?? "localhost",
|
|
406
|
+
port: storeConfig.port ?? 6379,
|
|
407
|
+
password: storeConfig.password,
|
|
408
|
+
db: storeConfig.db
|
|
409
|
+
},
|
|
410
|
+
defaultTTL: globalOptions.defaultTTL
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
break;
|
|
415
|
+
case "redis":
|
|
416
|
+
case "redis-client":
|
|
417
|
+
providers.push({
|
|
418
|
+
name: "cache:redis",
|
|
419
|
+
provide: CacheStoreToken,
|
|
420
|
+
useValue: new CacheRedisProvider(options)
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
case "memory":
|
|
424
|
+
providers.push({
|
|
425
|
+
name: "cache:memory",
|
|
426
|
+
provide: CacheStoreToken,
|
|
427
|
+
useValue: new CacheMemoryProvider(options.defaultTTL)
|
|
428
|
+
});
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
return providers;
|
|
432
|
+
});
|
|
433
|
+
__publicField(CachePlugin, "defaultOptions", {
|
|
434
|
+
type: "memory"
|
|
435
|
+
});
|
|
436
|
+
__decorateClass([
|
|
437
|
+
ToolHook.Will("execute", { priority: 1e3 })
|
|
438
|
+
], CachePlugin.prototype, "willReadCache", 1);
|
|
439
|
+
__decorateClass([
|
|
440
|
+
ToolHook.Did("execute", { priority: 1e3 })
|
|
441
|
+
], CachePlugin.prototype, "willWriteCache", 1);
|
|
442
|
+
CachePlugin = __decorateClass([
|
|
443
|
+
Plugin({
|
|
444
|
+
name: "cache",
|
|
445
|
+
description: "Cache plugin for caching tool results",
|
|
446
|
+
providers: [
|
|
447
|
+
/* add providers that always loaded with the plugin or default providers */
|
|
448
|
+
{
|
|
449
|
+
// this is a default provider for cache, will be overridden if dynamicProviders based on config
|
|
450
|
+
name: "cache:memory",
|
|
451
|
+
provide: CacheStoreToken,
|
|
452
|
+
useValue: new CacheMemoryProvider(60 * 60 * 24)
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
})
|
|
456
|
+
], CachePlugin);
|
|
457
|
+
function hashObject(obj) {
|
|
458
|
+
const keys = Object.keys(obj).sort();
|
|
459
|
+
return keys.reduce((acc, key) => {
|
|
460
|
+
acc += key + ":";
|
|
461
|
+
const val = obj[key];
|
|
462
|
+
if (typeof val === "object" && val !== null) {
|
|
463
|
+
acc += hashObject(val);
|
|
464
|
+
} else {
|
|
465
|
+
acc += String(val);
|
|
466
|
+
}
|
|
467
|
+
acc += ";";
|
|
468
|
+
return acc;
|
|
469
|
+
}, "");
|
|
470
|
+
}
|
|
471
|
+
export {
|
|
472
|
+
CachePlugin,
|
|
473
|
+
CachePlugin as default
|
|
474
|
+
};
|
package/esm/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frontmcp/plugin-cache",
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Cache plugin for FrontMCP - Redis, Vercel KV, and in-memory caching with automatic tool result caching",
|
|
5
|
+
"author": "AgentFront <info@agentfront.dev>",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"cache",
|
|
10
|
+
"redis",
|
|
11
|
+
"vercel-kv",
|
|
12
|
+
"plugin",
|
|
13
|
+
"frontmcp",
|
|
14
|
+
"agentfront"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/agentfront/frontmcp.git",
|
|
19
|
+
"directory": "plugins/plugin-cache"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/agentfront/frontmcp/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/agentfront/frontmcp/blob/main/plugins/plugin-cache/README.md",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "../index.js",
|
|
31
|
+
"module": "./index.mjs",
|
|
32
|
+
"types": "../index.d.ts",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "../package.json",
|
|
36
|
+
".": {
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "../index.d.ts",
|
|
39
|
+
"default": "../index.js"
|
|
40
|
+
},
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "../index.d.ts",
|
|
43
|
+
"default": "./index.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"ioredis": "^5.8.0",
|
|
49
|
+
"@frontmcp/sdk": "0.7.1",
|
|
50
|
+
"zod": "^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@vercel/kv": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"reflect-metadata": "^0.2.2"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/index.d.ts
ADDED
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACjE,cAAc,eAAe,CAAC"}
|
package/index.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
31
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
32
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
33
|
+
if (decorator = decorators[i])
|
|
34
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
35
|
+
if (kind && result) __defProp(target, key, result);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
39
|
+
|
|
40
|
+
// plugins/plugin-cache/src/index.ts
|
|
41
|
+
var index_exports = {};
|
|
42
|
+
__export(index_exports, {
|
|
43
|
+
CachePlugin: () => CachePlugin,
|
|
44
|
+
default: () => CachePlugin
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(index_exports);
|
|
47
|
+
|
|
48
|
+
// plugins/plugin-cache/src/cache.plugin.ts
|
|
49
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
50
|
+
|
|
51
|
+
// plugins/plugin-cache/src/providers/cache-redis.provider.ts
|
|
52
|
+
var import_ioredis = __toESM(require("ioredis"));
|
|
53
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
54
|
+
var CacheRedisProvider = class {
|
|
55
|
+
client;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
if (options.type !== "redis" && options.type !== "redis-client") {
|
|
58
|
+
throw new Error("Invalid cache provider type");
|
|
59
|
+
}
|
|
60
|
+
if (options.type === "redis-client") {
|
|
61
|
+
this.client = options.client;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.client = new import_ioredis.default({
|
|
65
|
+
lazyConnect: false,
|
|
66
|
+
maxRetriesPerRequest: 3,
|
|
67
|
+
...options.config
|
|
68
|
+
});
|
|
69
|
+
this.client.on("connect", () => console.log("[Redis] Connected"));
|
|
70
|
+
this.client.on("error", (err) => console.error("[Redis] Error:", err));
|
|
71
|
+
}
|
|
72
|
+
/** Set a value (auto-stringifies objects) */
|
|
73
|
+
async setValue(key, value, ttlSeconds) {
|
|
74
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
75
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
76
|
+
await this.client.set(key, strValue, "EX", ttlSeconds);
|
|
77
|
+
} else {
|
|
78
|
+
await this.client.set(key, strValue);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Get a value and automatically parse JSON if possible */
|
|
82
|
+
async getValue(key, defaultValue) {
|
|
83
|
+
const raw = await this.client.get(key);
|
|
84
|
+
if (raw === null) return defaultValue;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
return raw;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Delete a key */
|
|
92
|
+
async delete(key) {
|
|
93
|
+
await this.client.del(key);
|
|
94
|
+
}
|
|
95
|
+
/** Check if a key exists */
|
|
96
|
+
async exists(key) {
|
|
97
|
+
return await this.client.exists(key) === 1;
|
|
98
|
+
}
|
|
99
|
+
/** Gracefully close the Redis connection */
|
|
100
|
+
async close() {
|
|
101
|
+
await this.client.quit();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
CacheRedisProvider = __decorateClass([
|
|
105
|
+
(0, import_sdk.Provider)({
|
|
106
|
+
name: "provider:cache:redis",
|
|
107
|
+
description: "Redis-based cache provider",
|
|
108
|
+
scope: import_sdk.ProviderScope.GLOBAL
|
|
109
|
+
})
|
|
110
|
+
], CacheRedisProvider);
|
|
111
|
+
|
|
112
|
+
// plugins/plugin-cache/src/providers/cache-memory.provider.ts
|
|
113
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
114
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
115
|
+
var CacheMemoryProvider = class {
|
|
116
|
+
memory = /* @__PURE__ */ new Map();
|
|
117
|
+
sweeper;
|
|
118
|
+
constructor(sweepIntervalTTL = 60) {
|
|
119
|
+
this.sweeper = setInterval(() => this.sweep(), sweepIntervalTTL * 1e3);
|
|
120
|
+
this.sweeper.unref?.();
|
|
121
|
+
}
|
|
122
|
+
/** Set a value (auto-stringifies objects) */
|
|
123
|
+
async setValue(key, value, ttlSeconds) {
|
|
124
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
125
|
+
const existing = this.memory.get(key);
|
|
126
|
+
if (existing?.timeout) clearTimeout(existing.timeout);
|
|
127
|
+
const entry = { value: strValue };
|
|
128
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
129
|
+
const ttlMs = ttlSeconds * 1e3;
|
|
130
|
+
entry.expiresAt = Date.now() + ttlMs;
|
|
131
|
+
if (ttlMs <= MAX_TIMEOUT_MS) {
|
|
132
|
+
entry.timeout = setTimeout(() => {
|
|
133
|
+
const e = this.memory.get(key);
|
|
134
|
+
if (e && e.expiresAt && e.expiresAt <= Date.now()) {
|
|
135
|
+
this.memory.delete(key);
|
|
136
|
+
}
|
|
137
|
+
}, ttlMs);
|
|
138
|
+
entry.timeout.unref?.();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.memory.set(key, entry);
|
|
142
|
+
}
|
|
143
|
+
/** Get a value and automatically parse JSON if possible */
|
|
144
|
+
async getValue(key, defaultValue) {
|
|
145
|
+
const entry = this.memory.get(key);
|
|
146
|
+
if (!entry) return defaultValue;
|
|
147
|
+
if (this.isExpired(entry)) {
|
|
148
|
+
await this.delete(key);
|
|
149
|
+
return defaultValue;
|
|
150
|
+
}
|
|
151
|
+
const raw = entry.value;
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Delete a key */
|
|
159
|
+
async delete(key) {
|
|
160
|
+
const entry = this.memory.get(key);
|
|
161
|
+
if (entry?.timeout) clearTimeout(entry.timeout);
|
|
162
|
+
this.memory.delete(key);
|
|
163
|
+
}
|
|
164
|
+
/** Check if a key exists (and not expired) */
|
|
165
|
+
async exists(key) {
|
|
166
|
+
const entry = this.memory.get(key);
|
|
167
|
+
if (!entry) return false;
|
|
168
|
+
if (this.isExpired(entry)) {
|
|
169
|
+
await this.delete(key);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
/** Gracefully close the provider */
|
|
175
|
+
async close() {
|
|
176
|
+
if (this.sweeper) clearInterval(this.sweeper);
|
|
177
|
+
for (const [, entry] of this.memory) {
|
|
178
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
179
|
+
}
|
|
180
|
+
this.memory.clear();
|
|
181
|
+
}
|
|
182
|
+
// ---- internals ----
|
|
183
|
+
isExpired(entry) {
|
|
184
|
+
return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
|
|
185
|
+
}
|
|
186
|
+
/** Periodically remove expired keys to keep memory tidy */
|
|
187
|
+
sweep() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const [key, entry] of this.memory) {
|
|
190
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
|
|
191
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
192
|
+
this.memory.delete(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
CacheMemoryProvider = __decorateClass([
|
|
198
|
+
(0, import_sdk2.Provider)({
|
|
199
|
+
name: "provider:cache:memory",
|
|
200
|
+
description: "Memory-based cache provider",
|
|
201
|
+
scope: import_sdk2.ProviderScope.GLOBAL
|
|
202
|
+
})
|
|
203
|
+
], CacheMemoryProvider);
|
|
204
|
+
|
|
205
|
+
// plugins/plugin-cache/src/providers/cache-vercel-kv.provider.ts
|
|
206
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
207
|
+
var CacheVercelKvProvider = class {
|
|
208
|
+
kv;
|
|
209
|
+
keyPrefix;
|
|
210
|
+
defaultTTL;
|
|
211
|
+
constructor(options = {}) {
|
|
212
|
+
const vercelKv = require("@vercel/kv");
|
|
213
|
+
const hasUrl = options.url !== void 0;
|
|
214
|
+
const hasToken = options.token !== void 0;
|
|
215
|
+
if (hasUrl !== hasToken) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`CacheVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl ? "provided" : "missing"}, token=${hasToken ? "provided" : "missing"}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (options.url && options.token) {
|
|
221
|
+
this.kv = vercelKv.createClient({
|
|
222
|
+
url: options.url,
|
|
223
|
+
token: options.token
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
this.kv = vercelKv.kv;
|
|
227
|
+
}
|
|
228
|
+
this.keyPrefix = options.keyPrefix ?? "cache:";
|
|
229
|
+
this.defaultTTL = options.defaultTTL ?? 60 * 60 * 24;
|
|
230
|
+
}
|
|
231
|
+
prefixKey(key) {
|
|
232
|
+
return `${this.keyPrefix}${key}`;
|
|
233
|
+
}
|
|
234
|
+
/** Set a value (auto-stringifies objects) */
|
|
235
|
+
async setValue(key, value, ttlSeconds) {
|
|
236
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
237
|
+
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
238
|
+
if (ttl > 0) {
|
|
239
|
+
await this.kv.set(this.prefixKey(key), strValue, { ex: ttl });
|
|
240
|
+
} else {
|
|
241
|
+
await this.kv.set(this.prefixKey(key), strValue);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Get a value and automatically parse JSON if possible */
|
|
245
|
+
async getValue(key, defaultValue) {
|
|
246
|
+
const raw = await this.kv.get(this.prefixKey(key));
|
|
247
|
+
if (raw === null || raw === void 0) return defaultValue;
|
|
248
|
+
if (typeof raw === "string") {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(raw);
|
|
251
|
+
} catch {
|
|
252
|
+
return raw;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return raw;
|
|
256
|
+
}
|
|
257
|
+
/** Delete a key */
|
|
258
|
+
async delete(key) {
|
|
259
|
+
await this.kv.del(this.prefixKey(key));
|
|
260
|
+
}
|
|
261
|
+
/** Check if a key exists */
|
|
262
|
+
async exists(key) {
|
|
263
|
+
return await this.kv.exists(this.prefixKey(key)) === 1;
|
|
264
|
+
}
|
|
265
|
+
/** Gracefully close the provider (no-op for Vercel KV - stateless REST API) */
|
|
266
|
+
async close() {
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
CacheVercelKvProvider = __decorateClass([
|
|
270
|
+
(0, import_sdk3.Provider)({
|
|
271
|
+
name: "provider:cache:vercel-kv",
|
|
272
|
+
description: "Vercel KV-based cache provider",
|
|
273
|
+
scope: import_sdk3.ProviderScope.GLOBAL
|
|
274
|
+
})
|
|
275
|
+
], CacheVercelKvProvider);
|
|
276
|
+
|
|
277
|
+
// plugins/plugin-cache/src/cache.symbol.ts
|
|
278
|
+
var CacheStoreToken = /* @__PURE__ */ Symbol("plugin:cache:store");
|
|
279
|
+
|
|
280
|
+
// plugins/plugin-cache/src/cache.plugin.ts
|
|
281
|
+
var DEFAULT_BYPASS_HEADER = "x-frontmcp-disable-cache";
|
|
282
|
+
function matchesToolPattern(toolName, patterns) {
|
|
283
|
+
if (!patterns || patterns.length === 0) return false;
|
|
284
|
+
return patterns.some((pattern) => {
|
|
285
|
+
if (pattern.includes("*")) {
|
|
286
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
287
|
+
const regex = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
|
|
288
|
+
return regex.test(toolName);
|
|
289
|
+
}
|
|
290
|
+
return pattern === toolName;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
var CachePlugin = class extends import_sdk4.DynamicPlugin {
|
|
294
|
+
options;
|
|
295
|
+
constructor(options = CachePlugin.defaultOptions) {
|
|
296
|
+
super();
|
|
297
|
+
this.options = {
|
|
298
|
+
defaultTTL: 60 * 60 * 24,
|
|
299
|
+
...options
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Check if a tool should be cached based on metadata or tools list.
|
|
304
|
+
*/
|
|
305
|
+
shouldCacheTool(toolName, cacheMetadata) {
|
|
306
|
+
if (cacheMetadata) return true;
|
|
307
|
+
const patterns = this.options.toolPatterns ?? [];
|
|
308
|
+
return matchesToolPattern(toolName, patterns);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Check if cache should be bypassed based on request headers.
|
|
312
|
+
* Accesses headers from FrontMcpContextStorage which extracts x-frontmcp-* custom headers.
|
|
313
|
+
*/
|
|
314
|
+
shouldBypassCache(_flowCtx) {
|
|
315
|
+
const bypassHeader = this.options.bypassHeader ?? DEFAULT_BYPASS_HEADER;
|
|
316
|
+
try {
|
|
317
|
+
const contextStorage = this.get(import_sdk4.FrontMcpContextStorage);
|
|
318
|
+
const context = contextStorage?.getStore();
|
|
319
|
+
const customHeaders = context?.metadata?.customHeaders;
|
|
320
|
+
if (!customHeaders) return false;
|
|
321
|
+
const headerKey = bypassHeader.toLowerCase();
|
|
322
|
+
const headerValue = customHeaders[headerKey];
|
|
323
|
+
return headerValue === "true" || headerValue === "1";
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get TTL for a tool, with metadata taking precedence over defaults.
|
|
330
|
+
*/
|
|
331
|
+
getTtl(cacheMetadata) {
|
|
332
|
+
if (typeof cacheMetadata === "object" && cacheMetadata.ttl !== void 0) {
|
|
333
|
+
return cacheMetadata.ttl;
|
|
334
|
+
}
|
|
335
|
+
return this.options.defaultTTL ?? 60 * 60 * 24;
|
|
336
|
+
}
|
|
337
|
+
async willReadCache(flowCtx) {
|
|
338
|
+
const { tool, toolContext } = flowCtx.state;
|
|
339
|
+
if (!tool || !toolContext) return;
|
|
340
|
+
if (this.shouldBypassCache(flowCtx)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const { cache } = toolContext.metadata;
|
|
344
|
+
if (!this.shouldCacheTool(tool.fullName, cache) && !this.shouldCacheTool(tool.name, cache) || typeof toolContext.input === "undefined") {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const cacheStore = this.get(CacheStoreToken);
|
|
348
|
+
const hash = hashObject({ tool: tool.fullName, input: toolContext.input });
|
|
349
|
+
const cached = await cacheStore.getValue(hash);
|
|
350
|
+
if (cached !== void 0 && cached !== null) {
|
|
351
|
+
const cacheConfig = typeof cache === "object" ? cache : void 0;
|
|
352
|
+
if (cache === true || cacheConfig?.ttl && cacheConfig?.slideWindow) {
|
|
353
|
+
const ttl = this.getTtl(cache);
|
|
354
|
+
await cacheStore.setValue(hash, cached, ttl);
|
|
355
|
+
}
|
|
356
|
+
if (!tool.safeParseOutput(cached).success) {
|
|
357
|
+
await cacheStore.delete(hash);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const isPlainObject = typeof cached === "object" && cached !== null && !Array.isArray(cached);
|
|
361
|
+
let cachedWithMeta;
|
|
362
|
+
if (isPlainObject) {
|
|
363
|
+
const cachedRecord = cached;
|
|
364
|
+
const existingMeta = cachedRecord["_meta"] || {};
|
|
365
|
+
cachedWithMeta = {
|
|
366
|
+
...cachedRecord,
|
|
367
|
+
_meta: {
|
|
368
|
+
...existingMeta,
|
|
369
|
+
cache: "hit"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
} else {
|
|
373
|
+
cachedWithMeta = cached;
|
|
374
|
+
}
|
|
375
|
+
flowCtx.state.rawOutput = cachedWithMeta;
|
|
376
|
+
toolContext.respond(cachedWithMeta);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async willWriteCache(flowCtx) {
|
|
380
|
+
const { tool, toolContext } = flowCtx.state;
|
|
381
|
+
if (!tool || !toolContext) return;
|
|
382
|
+
if (this.shouldBypassCache(flowCtx)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const { cache } = toolContext.metadata;
|
|
386
|
+
const shouldCache = this.shouldCacheTool(tool.fullName, cache) || this.shouldCacheTool(tool.name, cache);
|
|
387
|
+
if (!shouldCache || typeof toolContext.input === "undefined") {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const cacheStore = this.get(CacheStoreToken);
|
|
391
|
+
const ttl = this.getTtl(cache);
|
|
392
|
+
const hash = hashObject({ tool: tool.fullName, input: toolContext.input });
|
|
393
|
+
await cacheStore.setValue(hash, toolContext.output, ttl);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if a tool is cacheable based on metadata or tools list.
|
|
397
|
+
* This can be used by other plugins or flows to determine cacheability.
|
|
398
|
+
*/
|
|
399
|
+
isCacheable(toolName) {
|
|
400
|
+
return matchesToolPattern(toolName, this.options.toolPatterns ?? []);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
__publicField(CachePlugin, "dynamicProviders", (options) => {
|
|
404
|
+
const providers = [];
|
|
405
|
+
switch (options.type) {
|
|
406
|
+
case "global-store":
|
|
407
|
+
providers.push({
|
|
408
|
+
name: "cache:global-store",
|
|
409
|
+
provide: CacheStoreToken,
|
|
410
|
+
inject: () => [import_sdk4.FrontMcpConfig],
|
|
411
|
+
useFactory: (config) => {
|
|
412
|
+
const storeConfig = (0, import_sdk4.getGlobalStoreConfig)("CachePlugin", config);
|
|
413
|
+
const globalOptions = options;
|
|
414
|
+
if ((0, import_sdk4.isVercelKvProvider)(storeConfig)) {
|
|
415
|
+
return new CacheVercelKvProvider({
|
|
416
|
+
url: storeConfig.url,
|
|
417
|
+
token: storeConfig.token,
|
|
418
|
+
keyPrefix: storeConfig.keyPrefix,
|
|
419
|
+
defaultTTL: globalOptions.defaultTTL
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return new CacheRedisProvider({
|
|
423
|
+
type: "redis",
|
|
424
|
+
config: {
|
|
425
|
+
host: storeConfig.host ?? "localhost",
|
|
426
|
+
port: storeConfig.port ?? 6379,
|
|
427
|
+
password: storeConfig.password,
|
|
428
|
+
db: storeConfig.db
|
|
429
|
+
},
|
|
430
|
+
defaultTTL: globalOptions.defaultTTL
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
break;
|
|
435
|
+
case "redis":
|
|
436
|
+
case "redis-client":
|
|
437
|
+
providers.push({
|
|
438
|
+
name: "cache:redis",
|
|
439
|
+
provide: CacheStoreToken,
|
|
440
|
+
useValue: new CacheRedisProvider(options)
|
|
441
|
+
});
|
|
442
|
+
break;
|
|
443
|
+
case "memory":
|
|
444
|
+
providers.push({
|
|
445
|
+
name: "cache:memory",
|
|
446
|
+
provide: CacheStoreToken,
|
|
447
|
+
useValue: new CacheMemoryProvider(options.defaultTTL)
|
|
448
|
+
});
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
return providers;
|
|
452
|
+
});
|
|
453
|
+
__publicField(CachePlugin, "defaultOptions", {
|
|
454
|
+
type: "memory"
|
|
455
|
+
});
|
|
456
|
+
__decorateClass([
|
|
457
|
+
import_sdk4.ToolHook.Will("execute", { priority: 1e3 })
|
|
458
|
+
], CachePlugin.prototype, "willReadCache", 1);
|
|
459
|
+
__decorateClass([
|
|
460
|
+
import_sdk4.ToolHook.Did("execute", { priority: 1e3 })
|
|
461
|
+
], CachePlugin.prototype, "willWriteCache", 1);
|
|
462
|
+
CachePlugin = __decorateClass([
|
|
463
|
+
(0, import_sdk4.Plugin)({
|
|
464
|
+
name: "cache",
|
|
465
|
+
description: "Cache plugin for caching tool results",
|
|
466
|
+
providers: [
|
|
467
|
+
/* add providers that always loaded with the plugin or default providers */
|
|
468
|
+
{
|
|
469
|
+
// this is a default provider for cache, will be overridden if dynamicProviders based on config
|
|
470
|
+
name: "cache:memory",
|
|
471
|
+
provide: CacheStoreToken,
|
|
472
|
+
useValue: new CacheMemoryProvider(60 * 60 * 24)
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
})
|
|
476
|
+
], CachePlugin);
|
|
477
|
+
function hashObject(obj) {
|
|
478
|
+
const keys = Object.keys(obj).sort();
|
|
479
|
+
return keys.reduce((acc, key) => {
|
|
480
|
+
acc += key + ":";
|
|
481
|
+
const val = obj[key];
|
|
482
|
+
if (typeof val === "object" && val !== null) {
|
|
483
|
+
acc += hashObject(val);
|
|
484
|
+
} else {
|
|
485
|
+
acc += String(val);
|
|
486
|
+
}
|
|
487
|
+
acc += ";";
|
|
488
|
+
return acc;
|
|
489
|
+
}, "");
|
|
490
|
+
}
|
|
491
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
492
|
+
0 && (module.exports = {
|
|
493
|
+
CachePlugin
|
|
494
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-cache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Cache plugin for FrontMCP - Redis, Vercel KV, and in-memory caching with automatic tool result caching",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"ioredis": "^5.8.0",
|
|
49
|
-
"@frontmcp/sdk": "0.7.
|
|
49
|
+
"@frontmcp/sdk": "0.7.1",
|
|
50
50
|
"zod": "^4.0.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CacheStoreInterface } from '../cache.types';
|
|
2
|
+
export default class CacheMemoryProvider implements CacheStoreInterface {
|
|
3
|
+
private readonly memory;
|
|
4
|
+
private sweeper?;
|
|
5
|
+
constructor(sweepIntervalTTL?: number);
|
|
6
|
+
/** Set a value (auto-stringifies objects) */
|
|
7
|
+
setValue(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
8
|
+
/** Get a value and automatically parse JSON if possible */
|
|
9
|
+
getValue<T = unknown>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
10
|
+
/** Delete a key */
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
/** Check if a key exists (and not expired) */
|
|
13
|
+
exists(key: string): Promise<boolean>;
|
|
14
|
+
/** Gracefully close the provider */
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
private isExpired;
|
|
17
|
+
/** Periodically remove expired keys to keep memory tidy */
|
|
18
|
+
private sweep;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=cache-memory.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-memory.provider.d.ts","sourceRoot":"","sources":["../../src/providers/cache-memory.provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAiBrD,MAAM,CAAC,OAAO,OAAO,mBAAoB,YAAW,mBAAmB;IACrE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,OAAO,CAAC,CAAiB;gBAErB,gBAAgB,SAAK;IAMjC,6CAA6C;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B/E,2DAA2D;IACrD,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAkBlF,mBAAmB;IACb,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxC,8CAA8C;IACxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAU3C,oCAAoC;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAU5B,OAAO,CAAC,SAAS;IAIjB,2DAA2D;IAC3D,OAAO,CAAC,KAAK;CASd"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CacheStoreInterface, RedisCacheOptions } from '../cache.types';
|
|
2
|
+
export default class CacheRedisProvider implements CacheStoreInterface {
|
|
3
|
+
private readonly client;
|
|
4
|
+
constructor(options: RedisCacheOptions);
|
|
5
|
+
/** Set a value (auto-stringifies objects) */
|
|
6
|
+
setValue(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
7
|
+
/** Get a value and automatically parse JSON if possible */
|
|
8
|
+
getValue<T = unknown>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
9
|
+
/** Delete a key */
|
|
10
|
+
delete(key: string): Promise<void>;
|
|
11
|
+
/** Check if a key exists */
|
|
12
|
+
exists(key: string): Promise<boolean>;
|
|
13
|
+
/** Gracefully close the Redis connection */
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=cache-redis.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-redis.provider.d.ts","sourceRoot":"","sources":["../../src/providers/cache-redis.provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAOxE,MAAM,CAAC,OAAO,OAAO,kBAAmB,YAAW,mBAAmB;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,EAAE,iBAAiB;IAmBtC,6CAA6C;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS/E,2DAA2D;IACrD,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAYlF,mBAAmB;IACb,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,4BAA4B;IACtB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3C,4CAA4C;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CacheStoreInterface } from '../cache.types';
|
|
2
|
+
export interface CacheVercelKvProviderOptions {
|
|
3
|
+
url?: string;
|
|
4
|
+
token?: string;
|
|
5
|
+
keyPrefix?: string;
|
|
6
|
+
defaultTTL?: number;
|
|
7
|
+
}
|
|
8
|
+
export default class CacheVercelKvProvider implements CacheStoreInterface {
|
|
9
|
+
private kv;
|
|
10
|
+
private readonly keyPrefix;
|
|
11
|
+
private readonly defaultTTL;
|
|
12
|
+
constructor(options?: CacheVercelKvProviderOptions);
|
|
13
|
+
private prefixKey;
|
|
14
|
+
/** Set a value (auto-stringifies objects) */
|
|
15
|
+
setValue(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
16
|
+
/** Get a value and automatically parse JSON if possible */
|
|
17
|
+
getValue<T = unknown>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
18
|
+
/** Delete a key */
|
|
19
|
+
delete(key: string): Promise<void>;
|
|
20
|
+
/** Check if a key exists */
|
|
21
|
+
exists(key: string): Promise<boolean>;
|
|
22
|
+
/** Gracefully close the provider (no-op for Vercel KV - stateless REST API) */
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=cache-vercel-kv.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-vercel-kv.provider.d.ts","sourceRoot":"","sources":["../../src/providers/cache-vercel-kv.provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,WAAW,4BAA4B;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAeD,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,mBAAmB;IACvE,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,GAAE,4BAAiC;IA6BtD,OAAO,CAAC,SAAS;IAIjB,6CAA6C;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/E,2DAA2D;IACrD,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAgBlF,mBAAmB;IACb,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,4BAA4B;IACtB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3C,+EAA+E;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|