veryfront 0.0.82 → 0.0.84
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/README.md +18 -17
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +2 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +2 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/multi-tier.d.ts +0 -29
- package/esm/src/cache/multi-tier.d.ts.map +1 -1
- package/esm/src/cache/multi-tier.js +0 -26
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/commands/dev.js +2 -2
- package/esm/src/cli/commands/new.js +1 -1
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/ui/tui.js +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +139 -64
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +3 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/multi-tier.ts +0 -41
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/commands/dev.ts +2 -2
- package/src/src/cli/commands/new.ts +1 -1
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/ui/tui.ts +1 -1
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +148 -73
- package/src/src/utils/index.ts +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Token Cache
|
|
3
|
+
*
|
|
4
|
+
* Uses the standard `redis` package for cross-runtime compatibility.
|
|
5
|
+
* Works in Deno, Node.js, and Bun.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createClient, type RedisClientType } from "redis";
|
|
9
|
+
import type { CacheStats, RedisCacheOptions, TokenCache, TokenCacheEntry } from "./types.js";
|
|
10
|
+
import { withSpan } from "../tracing.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PREFIX = "vf:token:";
|
|
13
|
+
const DEFAULT_CONNECT_TIMEOUT = 5000;
|
|
14
|
+
const DEFAULT_SCAN_COUNT = 100;
|
|
15
|
+
|
|
16
|
+
export class RedisCache implements TokenCache {
|
|
17
|
+
private client: RedisClientType | null = null;
|
|
18
|
+
private prefix: string;
|
|
19
|
+
private url: string;
|
|
20
|
+
private connectTimeout: number;
|
|
21
|
+
private hits = 0;
|
|
22
|
+
private misses = 0;
|
|
23
|
+
private connected = false;
|
|
24
|
+
|
|
25
|
+
constructor(options: RedisCacheOptions) {
|
|
26
|
+
this.url = options.url;
|
|
27
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
28
|
+
this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private key(k: string): string {
|
|
32
|
+
return `${this.prefix}${k}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(key: string): Promise<TokenCacheEntry | null> {
|
|
36
|
+
return withSpan("cache.redis.get", async () => {
|
|
37
|
+
try {
|
|
38
|
+
await this.ensureConnected();
|
|
39
|
+
const data = await this.client!.get(this.key(key));
|
|
40
|
+
|
|
41
|
+
if (!data) {
|
|
42
|
+
this.misses++;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entry = JSON.parse(data) as TokenCacheEntry;
|
|
47
|
+
|
|
48
|
+
if (Date.now() >= entry.expiresAt) {
|
|
49
|
+
await this.client!.del(this.key(key));
|
|
50
|
+
this.misses++;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.hits++;
|
|
55
|
+
return entry;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("[RedisCache] Get error:", error);
|
|
58
|
+
this.connected = false;
|
|
59
|
+
this.misses++;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}, { "cache.key": key });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async set(key: string, entry: TokenCacheEntry): Promise<void> {
|
|
66
|
+
return withSpan("cache.redis.set", async () => {
|
|
67
|
+
try {
|
|
68
|
+
await this.ensureConnected();
|
|
69
|
+
const ttlMs = entry.expiresAt - Date.now();
|
|
70
|
+
const ttlSeconds = Math.max(1, Math.floor(ttlMs / 1000));
|
|
71
|
+
await this.client!.setEx(this.key(key), ttlSeconds, JSON.stringify(entry));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("[RedisCache] Set error:", error);
|
|
74
|
+
this.connected = false;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}, { "cache.key": key });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(key: string): Promise<void> {
|
|
81
|
+
return withSpan("cache.redis.delete", async () => {
|
|
82
|
+
try {
|
|
83
|
+
await this.ensureConnected();
|
|
84
|
+
await this.client!.del(this.key(key));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("[RedisCache] Delete error:", error);
|
|
87
|
+
this.connected = false;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}, { "cache.key": key });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async clear(): Promise<void> {
|
|
94
|
+
return withSpan("cache.redis.clear", async () => {
|
|
95
|
+
try {
|
|
96
|
+
await this.ensureConnected();
|
|
97
|
+
|
|
98
|
+
const pattern = `${this.prefix}*`;
|
|
99
|
+
let cursor = "0";
|
|
100
|
+
let totalDeleted = 0;
|
|
101
|
+
|
|
102
|
+
do {
|
|
103
|
+
const result = await this.client!.scan(cursor, {
|
|
104
|
+
MATCH: pattern,
|
|
105
|
+
COUNT: DEFAULT_SCAN_COUNT,
|
|
106
|
+
});
|
|
107
|
+
cursor = String(result.cursor);
|
|
108
|
+
|
|
109
|
+
if (result.keys.length > 0) {
|
|
110
|
+
totalDeleted += await this.client!.del(result.keys);
|
|
111
|
+
}
|
|
112
|
+
} while (cursor !== "0");
|
|
113
|
+
|
|
114
|
+
if (totalDeleted > 0) {
|
|
115
|
+
console.log(`[RedisCache] Cleared ${totalDeleted} keys`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.hits = 0;
|
|
119
|
+
this.misses = 0;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error("[RedisCache] Clear error:", error);
|
|
122
|
+
this.connected = false;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async has(key: string): Promise<boolean> {
|
|
129
|
+
return withSpan("cache.redis.has", async () => {
|
|
130
|
+
try {
|
|
131
|
+
await this.ensureConnected();
|
|
132
|
+
const exists = await this.client!.exists(this.key(key));
|
|
133
|
+
return exists === 1;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("[RedisCache] Has error:", error);
|
|
136
|
+
this.connected = false;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}, { "cache.key": key });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async stats(): Promise<CacheStats> {
|
|
143
|
+
return withSpan("cache.redis.stats", async () => {
|
|
144
|
+
let size = 0;
|
|
145
|
+
try {
|
|
146
|
+
await this.ensureConnected();
|
|
147
|
+
size = await this.client!.dbSize();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.connected = false;
|
|
150
|
+
console.warn("[RedisCache] Stats error:", error);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { hits: this.hits, misses: this.misses, size, type: "redis" as const };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async close(): Promise<void> {
|
|
158
|
+
return withSpan("cache.redis.close", async () => {
|
|
159
|
+
if (this.client) {
|
|
160
|
+
try {
|
|
161
|
+
await this.client.quit();
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore close errors
|
|
164
|
+
}
|
|
165
|
+
this.client = null;
|
|
166
|
+
}
|
|
167
|
+
this.connected = false;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async ensureConnected(): Promise<void> {
|
|
172
|
+
return withSpan("cache.redis.connect", async () => {
|
|
173
|
+
if (this.connected && this.client) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create client with connection options
|
|
178
|
+
this.client = createClient({
|
|
179
|
+
url: this.url,
|
|
180
|
+
socket: {
|
|
181
|
+
connectTimeout: this.connectTimeout,
|
|
182
|
+
reconnectStrategy: (retries) => {
|
|
183
|
+
// Exponential backoff with max 3 retries
|
|
184
|
+
if (retries > 3) {
|
|
185
|
+
return new Error("Max reconnection attempts reached");
|
|
186
|
+
}
|
|
187
|
+
return Math.min(retries * 100, 3000);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Handle connection errors
|
|
193
|
+
this.client.on("error", (err) => {
|
|
194
|
+
console.error("[RedisCache] Client error:", err);
|
|
195
|
+
this.connected = false;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await this.client.connect();
|
|
199
|
+
this.connected = true;
|
|
200
|
+
console.log("[RedisCache] Connected");
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient Token Cache
|
|
3
|
+
*
|
|
4
|
+
* Wraps a primary cache (Redis) with a fallback cache (Memory).
|
|
5
|
+
* Automatically falls back to memory cache when Redis operations fail.
|
|
6
|
+
* Provides graceful degradation instead of hard failures.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CacheStats, TokenCache, TokenCacheEntry } from "./types.js";
|
|
10
|
+
import { proxyLogger } from "../logger.js";
|
|
11
|
+
import { withSpan } from "../tracing.js";
|
|
12
|
+
|
|
13
|
+
const CIRCUIT_OPEN_DURATION_MS = 30_000; // 30 seconds
|
|
14
|
+
const FAILURE_THRESHOLD = 3; // failures before circuit opens
|
|
15
|
+
|
|
16
|
+
const logger = proxyLogger.child({ module: "cache" });
|
|
17
|
+
|
|
18
|
+
export class ResilientCache implements TokenCache {
|
|
19
|
+
private primary: TokenCache;
|
|
20
|
+
private fallback: TokenCache;
|
|
21
|
+
private usingFallback = false;
|
|
22
|
+
private failureCount = 0;
|
|
23
|
+
private circuitOpenedAt: number | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(primary: TokenCache, fallback: TokenCache) {
|
|
26
|
+
this.primary = primary;
|
|
27
|
+
this.fallback = fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if we should try primary again after circuit was opened.
|
|
32
|
+
*/
|
|
33
|
+
private shouldTryPrimary(): boolean {
|
|
34
|
+
if (!this.usingFallback) return true;
|
|
35
|
+
|
|
36
|
+
// If circuit was opened, check if enough time has passed
|
|
37
|
+
if (this.circuitOpenedAt) {
|
|
38
|
+
const elapsed = Date.now() - this.circuitOpenedAt;
|
|
39
|
+
if (elapsed >= CIRCUIT_OPEN_DURATION_MS) {
|
|
40
|
+
logger.info("[ResilientCache] Circuit half-open, trying primary again");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Record a successful primary operation - reset failure state.
|
|
50
|
+
*/
|
|
51
|
+
private recordSuccess(): void {
|
|
52
|
+
if (this.usingFallback) {
|
|
53
|
+
logger.info("[ResilientCache] Primary recovered, switching back from fallback");
|
|
54
|
+
this.usingFallback = false;
|
|
55
|
+
this.failureCount = 0;
|
|
56
|
+
this.circuitOpenedAt = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record a primary failure - may trigger fallback.
|
|
62
|
+
*/
|
|
63
|
+
private recordFailure(error: unknown): void {
|
|
64
|
+
this.failureCount++;
|
|
65
|
+
logger.warn(
|
|
66
|
+
`[ResilientCache] Primary cache error (${this.failureCount}/${FAILURE_THRESHOLD}):`,
|
|
67
|
+
{ error: error instanceof Error ? error.message : error },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (this.failureCount >= FAILURE_THRESHOLD && !this.usingFallback) {
|
|
71
|
+
logger.warn("[ResilientCache] Opening circuit, switching to fallback cache");
|
|
72
|
+
this.usingFallback = true;
|
|
73
|
+
this.circuitOpenedAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async get(key: string): Promise<TokenCacheEntry | null> {
|
|
78
|
+
return withSpan("cache.resilient.get", async () => {
|
|
79
|
+
// Try primary if circuit allows
|
|
80
|
+
if (this.shouldTryPrimary()) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await this.primary.get(key);
|
|
83
|
+
this.recordSuccess();
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.recordFailure(error);
|
|
87
|
+
// Don't return here - try fallback
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Use fallback
|
|
92
|
+
return this.fallback.get(key);
|
|
93
|
+
}, { "cache.key": key, "cache.usingFallback": this.usingFallback });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async set(key: string, entry: TokenCacheEntry): Promise<void> {
|
|
97
|
+
return withSpan("cache.resilient.set", async () => {
|
|
98
|
+
// Always try to set in fallback (local cache)
|
|
99
|
+
await this.fallback.set(key, entry);
|
|
100
|
+
|
|
101
|
+
// Try primary if circuit allows
|
|
102
|
+
if (this.shouldTryPrimary()) {
|
|
103
|
+
try {
|
|
104
|
+
await this.primary.set(key, entry);
|
|
105
|
+
this.recordSuccess();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.recordFailure(error);
|
|
108
|
+
// Fallback already set above, no need to retry
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, { "cache.key": key, "cache.usingFallback": this.usingFallback });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async delete(key: string): Promise<void> {
|
|
115
|
+
return withSpan("cache.resilient.delete", async () => {
|
|
116
|
+
// Delete from both
|
|
117
|
+
await this.fallback.delete(key);
|
|
118
|
+
|
|
119
|
+
if (this.shouldTryPrimary()) {
|
|
120
|
+
try {
|
|
121
|
+
await this.primary.delete(key);
|
|
122
|
+
this.recordSuccess();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.recordFailure(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, { "cache.key": key });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async clear(): Promise<void> {
|
|
131
|
+
return withSpan("cache.resilient.clear", async () => {
|
|
132
|
+
await this.fallback.clear();
|
|
133
|
+
|
|
134
|
+
if (this.shouldTryPrimary()) {
|
|
135
|
+
try {
|
|
136
|
+
await this.primary.clear();
|
|
137
|
+
this.recordSuccess();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.recordFailure(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async has(key: string): Promise<boolean> {
|
|
146
|
+
return withSpan("cache.resilient.has", async () => {
|
|
147
|
+
if (this.shouldTryPrimary()) {
|
|
148
|
+
try {
|
|
149
|
+
const result = await this.primary.has(key);
|
|
150
|
+
this.recordSuccess();
|
|
151
|
+
return result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.recordFailure(error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return this.fallback.has(key);
|
|
158
|
+
}, { "cache.key": key });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async stats(): Promise<CacheStats> {
|
|
162
|
+
return withSpan("cache.resilient.stats", async () => {
|
|
163
|
+
const fallbackStats = await this.fallback.stats();
|
|
164
|
+
|
|
165
|
+
if (this.shouldTryPrimary()) {
|
|
166
|
+
try {
|
|
167
|
+
const primaryStats = await this.primary.stats();
|
|
168
|
+
this.recordSuccess();
|
|
169
|
+
return {
|
|
170
|
+
...primaryStats,
|
|
171
|
+
type: this.usingFallback ? "memory" as const : "redis" as const,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.recordFailure(error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...fallbackStats,
|
|
180
|
+
type: "memory" as const,
|
|
181
|
+
};
|
|
182
|
+
}, { "cache.usingFallback": this.usingFallback });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async close(): Promise<void> {
|
|
186
|
+
return withSpan("cache.resilient.close", async () => {
|
|
187
|
+
await Promise.all([this.primary.close(), this.fallback.close()]);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get current resilience status for debugging/health checks.
|
|
193
|
+
*/
|
|
194
|
+
getStatus(): {
|
|
195
|
+
usingFallback: boolean;
|
|
196
|
+
failureCount: number;
|
|
197
|
+
circuitOpenedAt: number | null;
|
|
198
|
+
} {
|
|
199
|
+
return {
|
|
200
|
+
usingFallback: this.usingFallback,
|
|
201
|
+
failureCount: this.failureCount,
|
|
202
|
+
circuitOpenedAt: this.circuitOpenedAt,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Cache Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstraction for storing OAuth tokens with TTL support.
|
|
5
|
+
* Implementations: MemoryCache, RedisCache
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TokenCacheEntry {
|
|
9
|
+
token: string;
|
|
10
|
+
expiresAt: number; // Unix timestamp in ms
|
|
11
|
+
scope: "preview" | "production";
|
|
12
|
+
projectSlug?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TokenCache {
|
|
16
|
+
/**
|
|
17
|
+
* Get a cached token entry.
|
|
18
|
+
*/
|
|
19
|
+
get(key: string): Promise<TokenCacheEntry | null>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set a token entry with automatic TTL based on expiresAt.
|
|
23
|
+
*/
|
|
24
|
+
set(key: string, entry: TokenCacheEntry): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Delete a specific token entry.
|
|
28
|
+
*/
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear all cached tokens.
|
|
33
|
+
*/
|
|
34
|
+
clear(): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a key exists in cache.
|
|
38
|
+
*/
|
|
39
|
+
has(key: string): Promise<boolean>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get cache statistics.
|
|
43
|
+
*/
|
|
44
|
+
stats(): Promise<CacheStats>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Close any connections (for Redis).
|
|
48
|
+
*/
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CacheStats {
|
|
53
|
+
hits: number;
|
|
54
|
+
misses: number;
|
|
55
|
+
size: number;
|
|
56
|
+
type: "memory" | "redis";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MemoryCacheOptions {
|
|
60
|
+
maxSize?: number; // Maximum number of entries
|
|
61
|
+
cleanupInterval?: number; // Interval in ms to cleanup expired entries
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RedisCacheOptions {
|
|
65
|
+
url: string;
|
|
66
|
+
prefix?: string;
|
|
67
|
+
connectTimeout?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type CacheOptions =
|
|
71
|
+
| { type: "memory"; options?: MemoryCacheOptions }
|
|
72
|
+
| { type: "redis"; options: RedisCacheOptions };
|