schemantic 0.1.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 +92 -0
- package/LICENSE +21 -0
- package/README.md +827 -0
- package/dist/__tests__/setup.d.ts +6 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +30 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/__tests__/test-config.d.ts +36 -0
- package/dist/__tests__/test-config.d.ts.map +1 -0
- package/dist/__tests__/test-config.js +563 -0
- package/dist/__tests__/test-config.js.map +1 -0
- package/dist/cli/index.d.ts +118 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +657 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +21 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/schemantic.d.ts +95 -0
- package/dist/core/schemantic.d.ts.map +1 -0
- package/dist/core/schemantic.js +543 -0
- package/dist/core/schemantic.js.map +1 -0
- package/dist/generators/api-client-generator.d.ts +145 -0
- package/dist/generators/api-client-generator.d.ts.map +1 -0
- package/dist/generators/api-client-generator.js +856 -0
- package/dist/generators/api-client-generator.js.map +1 -0
- package/dist/generators/base.d.ts +117 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/base.js +232 -0
- package/dist/generators/base.js.map +1 -0
- package/dist/generators/enum-generator.d.ts +52 -0
- package/dist/generators/enum-generator.d.ts.map +1 -0
- package/dist/generators/enum-generator.js +178 -0
- package/dist/generators/enum-generator.js.map +1 -0
- package/dist/generators/factory.d.ts +59 -0
- package/dist/generators/factory.d.ts.map +1 -0
- package/dist/generators/factory.js +127 -0
- package/dist/generators/factory.js.map +1 -0
- package/dist/generators/hook-generator.d.ts +36 -0
- package/dist/generators/hook-generator.d.ts.map +1 -0
- package/dist/generators/hook-generator.js +495 -0
- package/dist/generators/hook-generator.js.map +1 -0
- package/dist/generators/index.d.ts +10 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +26 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/object-generator.d.ts +48 -0
- package/dist/generators/object-generator.d.ts.map +1 -0
- package/dist/generators/object-generator.js +280 -0
- package/dist/generators/object-generator.js.map +1 -0
- package/dist/generators/primitive-generator.d.ts +40 -0
- package/dist/generators/primitive-generator.d.ts.map +1 -0
- package/dist/generators/primitive-generator.js +150 -0
- package/dist/generators/primitive-generator.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/base.d.ts +101 -0
- package/dist/parsers/base.d.ts.map +1 -0
- package/dist/parsers/base.js +77 -0
- package/dist/parsers/base.js.map +1 -0
- package/dist/parsers/factory.d.ts +64 -0
- package/dist/parsers/factory.d.ts.map +1 -0
- package/dist/parsers/factory.js +116 -0
- package/dist/parsers/factory.js.map +1 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +23 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/openapi.d.ts +52 -0
- package/dist/parsers/openapi.d.ts.map +1 -0
- package/dist/parsers/openapi.js +396 -0
- package/dist/parsers/openapi.js.map +1 -0
- package/dist/plugins/branded-types.d.ts +28 -0
- package/dist/plugins/branded-types.d.ts.map +1 -0
- package/dist/plugins/branded-types.js +568 -0
- package/dist/plugins/branded-types.js.map +1 -0
- package/dist/plugins/builtin.d.ts +38 -0
- package/dist/plugins/builtin.d.ts.map +1 -0
- package/dist/plugins/builtin.js +283 -0
- package/dist/plugins/builtin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +23 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/loader.d.ts +92 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/manager.d.ts +99 -0
- package/dist/plugins/manager.d.ts.map +1 -0
- package/dist/plugins/manager.js +233 -0
- package/dist/plugins/manager.js.map +1 -0
- package/dist/plugins/performance-monitoring.d.ts +98 -0
- package/dist/plugins/performance-monitoring.d.ts.map +1 -0
- package/dist/plugins/performance-monitoring.js +596 -0
- package/dist/plugins/performance-monitoring.js.map +1 -0
- package/dist/plugins/request-deduplication.d.ts +27 -0
- package/dist/plugins/request-deduplication.d.ts.map +1 -0
- package/dist/plugins/request-deduplication.js +650 -0
- package/dist/plugins/request-deduplication.js.map +1 -0
- package/dist/plugins/zod-validation.d.ts +63 -0
- package/dist/plugins/zod-validation.d.ts.map +1 -0
- package/dist/plugins/zod-validation.js +743 -0
- package/dist/plugins/zod-validation.js.map +1 -0
- package/dist/types/core.d.ts +203 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +24 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/openapi.d.ts +250 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +8 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/types/schema.d.ts +68 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +22 -0
- package/dist/types/schema.js.map +1 -0
- package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +355 -0
- package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +309 -0
- package/docs/API.md +600 -0
- package/docs/CONFIGURATION.md +722 -0
- package/docs/FASTAPI_TUTORIAL.md +1497 -0
- package/docs/PLUGINS.md +683 -0
- package/docs/PLUGIN_DEVELOPMENT.md +720 -0
- package/package.json +90 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Advanced Request Deduplication Plugin
|
|
4
|
+
*
|
|
5
|
+
* Implements intelligent request deduplication at the client level with
|
|
6
|
+
* sophisticated caching strategies, memory management, and performance optimization.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Request fingerprinting with content-based hashing
|
|
10
|
+
* - Intelligent cache invalidation strategies
|
|
11
|
+
* - Memory-efficient pending request management
|
|
12
|
+
* - Configurable TTL and size limits
|
|
13
|
+
* - Request coalescing for identical concurrent requests
|
|
14
|
+
* - Stale-while-revalidate patterns
|
|
15
|
+
*
|
|
16
|
+
* Architecture:
|
|
17
|
+
* - Non-blocking request deduplication
|
|
18
|
+
* - LRU cache with TTL support
|
|
19
|
+
* - WeakMap-based cleanup for memory efficiency
|
|
20
|
+
* - Configurable cache policies per endpoint
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.requestDeduplicationPlugin = void 0;
|
|
24
|
+
/**
|
|
25
|
+
* Advanced Request Deduplication Plugin Implementation
|
|
26
|
+
*/
|
|
27
|
+
exports.requestDeduplicationPlugin = {
|
|
28
|
+
name: "request-deduplication",
|
|
29
|
+
version: "2.0.0",
|
|
30
|
+
description: "Advanced request deduplication with intelligent caching and memory management",
|
|
31
|
+
/**
|
|
32
|
+
* Enhance API client with request deduplication capabilities
|
|
33
|
+
*/
|
|
34
|
+
afterClientGeneration: async (generatedClient, context) => {
|
|
35
|
+
const options = getPluginOptions(context);
|
|
36
|
+
if (!options.enabled) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
// Add deduplication middleware to client
|
|
41
|
+
const deduplicationCode = generateDeduplicationCode(generatedClient, options);
|
|
42
|
+
generatedClient.content += deduplicationCode;
|
|
43
|
+
// Update dependencies
|
|
44
|
+
generatedClient.dependencies.push("crypto-js"); // For hashing
|
|
45
|
+
// Add deduplication exports
|
|
46
|
+
generatedClient.exports.push("RequestDeduplicator", "CacheManager", "RequestCoalescer", "clearRequestCache", "getCacheStats");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn("Request deduplication plugin afterClientGeneration failed:", error);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Get plugin options from generation context
|
|
55
|
+
*/
|
|
56
|
+
function getPluginOptions(context) {
|
|
57
|
+
const pluginConfig = context?.config?.plugins?.find((p) => p.name === "request-deduplication");
|
|
58
|
+
return {
|
|
59
|
+
enabled: true,
|
|
60
|
+
defaultTTL: 5 * 60 * 1000, // 5 minutes
|
|
61
|
+
maxCacheSize: 1000,
|
|
62
|
+
maxPendingRequests: 100,
|
|
63
|
+
enableStaleWhileRevalidate: true,
|
|
64
|
+
enableCoalescing: true,
|
|
65
|
+
enableDebugLogging: false,
|
|
66
|
+
endpointPolicies: {},
|
|
67
|
+
...(pluginConfig?.options || {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generate request deduplication code for API client
|
|
72
|
+
*/
|
|
73
|
+
function generateDeduplicationCode(_generatedClient, options) {
|
|
74
|
+
return `
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Advanced Request Deduplication System
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* LRU Cache implementation with TTL support
|
|
82
|
+
*/
|
|
83
|
+
class LRUCacheWithTTL<K, V> {
|
|
84
|
+
private readonly maxSize: number;
|
|
85
|
+
private readonly cache = new Map<K, { value: V; timestamp: number; ttl: number }>();
|
|
86
|
+
private readonly accessOrder = new Map<K, number>();
|
|
87
|
+
private accessCounter = 0;
|
|
88
|
+
|
|
89
|
+
constructor(maxSize: number = ${options.maxCacheSize || 1000}) {
|
|
90
|
+
this.maxSize = maxSize;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
set(key: K, value: V, ttl: number = ${options.defaultTTL || 5 * 60 * 1000}): void {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
|
|
96
|
+
// Remove expired entries before adding new one
|
|
97
|
+
this.cleanup();
|
|
98
|
+
|
|
99
|
+
// If at capacity, remove LRU item
|
|
100
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
101
|
+
this.evictLRU();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.cache.set(key, {
|
|
105
|
+
value,
|
|
106
|
+
timestamp: now,
|
|
107
|
+
ttl
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.accessOrder.set(key, ++this.accessCounter);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get(key: K): V | undefined {
|
|
114
|
+
const entry = this.cache.get(key);
|
|
115
|
+
if (!entry) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
121
|
+
// Entry is expired
|
|
122
|
+
this.cache.delete(key);
|
|
123
|
+
this.accessOrder.delete(key);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Update access order
|
|
128
|
+
this.accessOrder.set(key, ++this.accessCounter);
|
|
129
|
+
return entry.value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
has(key: K): boolean {
|
|
133
|
+
return this.get(key) !== undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
delete(key: K): boolean {
|
|
137
|
+
const result = this.cache.delete(key);
|
|
138
|
+
this.accessOrder.delete(key);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
clear(): void {
|
|
143
|
+
this.cache.clear();
|
|
144
|
+
this.accessOrder.clear();
|
|
145
|
+
this.accessCounter = 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
size(): number {
|
|
149
|
+
this.cleanup();
|
|
150
|
+
return this.cache.size;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private cleanup(): void {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const expiredKeys: K[] = [];
|
|
156
|
+
|
|
157
|
+
for (const [key, entry] of this.cache) {
|
|
158
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
159
|
+
expiredKeys.push(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const key of expiredKeys) {
|
|
164
|
+
this.cache.delete(key);
|
|
165
|
+
this.accessOrder.delete(key);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private evictLRU(): void {
|
|
170
|
+
let oldestKey: K | undefined;
|
|
171
|
+
let oldestAccess = Infinity;
|
|
172
|
+
|
|
173
|
+
for (const [key, accessTime] of this.accessOrder) {
|
|
174
|
+
if (accessTime < oldestAccess) {
|
|
175
|
+
oldestAccess = accessTime;
|
|
176
|
+
oldestKey = key;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (oldestKey !== undefined) {
|
|
181
|
+
this.cache.delete(oldestKey);
|
|
182
|
+
this.accessOrder.delete(oldestKey);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getStats(): { size: number; hitRate: number; evictions: number } {
|
|
187
|
+
return {
|
|
188
|
+
size: this.cache.size,
|
|
189
|
+
hitRate: 0, // Would need to track hits/misses for this
|
|
190
|
+
evictions: 0 // Would need to track evictions for this
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Request fingerprint generator
|
|
197
|
+
*/
|
|
198
|
+
class RequestFingerprintGenerator {
|
|
199
|
+
/**
|
|
200
|
+
* Generate a unique fingerprint for a request
|
|
201
|
+
*/
|
|
202
|
+
static generate(
|
|
203
|
+
method: string,
|
|
204
|
+
url: string,
|
|
205
|
+
headers: Record<string, string> = {},
|
|
206
|
+
body?: unknown,
|
|
207
|
+
params?: Record<string, unknown>
|
|
208
|
+
): string {
|
|
209
|
+
const fingerprint = {
|
|
210
|
+
method: method.toUpperCase(),
|
|
211
|
+
url: this.normalizeURL(url),
|
|
212
|
+
headers: this.normalizeHeaders(headers),
|
|
213
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
214
|
+
params: params ? JSON.stringify(params) : undefined
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Simple hash generation (in production, use crypto-js or similar)
|
|
218
|
+
return this.simpleHash(JSON.stringify(fingerprint));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static normalizeURL(url: string): string {
|
|
222
|
+
try {
|
|
223
|
+
const urlObj = new URL(url);
|
|
224
|
+
// Sort query parameters for consistent fingerprinting
|
|
225
|
+
const params = new URLSearchParams(urlObj.search);
|
|
226
|
+
const sortedParams = new URLSearchParams();
|
|
227
|
+
Array.from(params.keys()).sort().forEach(key => {
|
|
228
|
+
params.getAll(key).forEach(value => sortedParams.append(key, value));
|
|
229
|
+
});
|
|
230
|
+
urlObj.search = sortedParams.toString();
|
|
231
|
+
return urlObj.toString();
|
|
232
|
+
} catch {
|
|
233
|
+
return url;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private static normalizeHeaders(headers: Record<string, string>): Record<string, string> {
|
|
238
|
+
const normalized: Record<string, string> = {};
|
|
239
|
+
|
|
240
|
+
// Only include cache-relevant headers and normalize case
|
|
241
|
+
const relevantHeaders = ['authorization', 'content-type', 'accept', 'cache-control'];
|
|
242
|
+
|
|
243
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
244
|
+
const lowerKey = key.toLowerCase();
|
|
245
|
+
if (relevantHeaders.includes(lowerKey)) {
|
|
246
|
+
normalized[lowerKey] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return normalized;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private static simpleHash(str: string): string {
|
|
254
|
+
let hash = 0;
|
|
255
|
+
for (let i = 0; i < str.length; i++) {
|
|
256
|
+
const char = str.charCodeAt(i);
|
|
257
|
+
hash = ((hash << 5) - hash) + char;
|
|
258
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
259
|
+
}
|
|
260
|
+
return hash.toString(36);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Request coalescing manager
|
|
266
|
+
*/
|
|
267
|
+
class RequestCoalescer {
|
|
268
|
+
private readonly pendingRequests = new Map<string, Promise<unknown>>();
|
|
269
|
+
private readonly maxPending: number;
|
|
270
|
+
|
|
271
|
+
constructor(maxPending = ${options.maxPendingRequests || 100}) {
|
|
272
|
+
this.maxPending = maxPending;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Coalesce identical requests
|
|
277
|
+
*/
|
|
278
|
+
async coalesce<T>(
|
|
279
|
+
fingerprint: string,
|
|
280
|
+
requestFactory: () => Promise<T>
|
|
281
|
+
): Promise<T> {
|
|
282
|
+
// Check if identical request is already pending
|
|
283
|
+
const existingRequest = this.pendingRequests.get(fingerprint);
|
|
284
|
+
if (existingRequest) {
|
|
285
|
+
${options.enableDebugLogging
|
|
286
|
+
? "console.debug(`Request coalesced: ${fingerprint}`);"
|
|
287
|
+
: ""}
|
|
288
|
+
return existingRequest as Promise<T>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check if we're at capacity
|
|
292
|
+
if (this.pendingRequests.size >= this.maxPending) {
|
|
293
|
+
${options.enableDebugLogging
|
|
294
|
+
? "console.warn(`Request coalescing at capacity, executing directly`);"
|
|
295
|
+
: ""}
|
|
296
|
+
return requestFactory();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create new request and track it
|
|
300
|
+
const requestPromise = requestFactory()
|
|
301
|
+
.finally(() => {
|
|
302
|
+
// Clean up when request completes
|
|
303
|
+
this.pendingRequests.delete(fingerprint);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.pendingRequests.set(fingerprint, requestPromise);
|
|
307
|
+
return requestPromise;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clear all pending requests
|
|
312
|
+
*/
|
|
313
|
+
clear(): void {
|
|
314
|
+
this.pendingRequests.clear();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get current pending request count
|
|
319
|
+
*/
|
|
320
|
+
getPendingCount(): number {
|
|
321
|
+
return this.pendingRequests.size;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Cache manager with stale-while-revalidate support
|
|
327
|
+
*/
|
|
328
|
+
class CacheManager<T = unknown> {
|
|
329
|
+
private readonly cache = new LRUCacheWithTTL<string, CacheEntry<T>>();
|
|
330
|
+
private readonly staleRevalidatePromises = new Map<string, Promise<T>>();
|
|
331
|
+
|
|
332
|
+
constructor(private readonly options: {
|
|
333
|
+
defaultTTL: number;
|
|
334
|
+
enableStaleWhileRevalidate: boolean;
|
|
335
|
+
debugLogging: boolean;
|
|
336
|
+
}) {}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get cached response or undefined if not cached/expired
|
|
340
|
+
*/
|
|
341
|
+
get(fingerprint: string): T | undefined {
|
|
342
|
+
const entry = this.cache.get(fingerprint);
|
|
343
|
+
|
|
344
|
+
if (!entry) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const age = now - entry.timestamp;
|
|
350
|
+
|
|
351
|
+
if (age > entry.ttl) {
|
|
352
|
+
// Entry is expired
|
|
353
|
+
if (this.options.enableStaleWhileRevalidate && !entry.stale) {
|
|
354
|
+
// Mark as stale but return it
|
|
355
|
+
entry.stale = true;
|
|
356
|
+
this.cache.set(fingerprint, entry, entry.ttl);
|
|
357
|
+
${options.enableDebugLogging
|
|
358
|
+
? "this.options.debugLogging && console.debug(`Serving stale data: ${fingerprint}`);"
|
|
359
|
+
: ""}
|
|
360
|
+
return entry.data;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Entry is too old even for stale-while-revalidate
|
|
364
|
+
this.cache.delete(fingerprint);
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
${options.enableDebugLogging
|
|
369
|
+
? "this.options.debugLogging && console.debug(`Cache hit: ${fingerprint}`);"
|
|
370
|
+
: ""}
|
|
371
|
+
return entry.data;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Set cached response
|
|
376
|
+
*/
|
|
377
|
+
set(fingerprint: string, data: T, ttl?: number): void {
|
|
378
|
+
const entry: CacheEntry<T> = {
|
|
379
|
+
data,
|
|
380
|
+
timestamp: Date.now(),
|
|
381
|
+
ttl: ttl || this.options.defaultTTL,
|
|
382
|
+
stale: false
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
this.cache.set(fingerprint, entry, entry.ttl);
|
|
386
|
+
${options.enableDebugLogging
|
|
387
|
+
? "this.options.debugLogging && console.debug(`Cache set: ${fingerprint}`);"
|
|
388
|
+
: ""}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Check if entry exists and is fresh
|
|
393
|
+
*/
|
|
394
|
+
hasFresh(fingerprint: string): boolean {
|
|
395
|
+
const entry = this.cache.get(fingerprint);
|
|
396
|
+
if (!entry) return false;
|
|
397
|
+
|
|
398
|
+
const age = Date.now() - entry.timestamp;
|
|
399
|
+
return age <= entry.ttl && !entry.stale;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if entry exists (including stale)
|
|
404
|
+
*/
|
|
405
|
+
has(fingerprint: string): boolean {
|
|
406
|
+
return this.cache.has(fingerprint);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Invalidate cache entry
|
|
411
|
+
*/
|
|
412
|
+
invalidate(fingerprint: string): void {
|
|
413
|
+
this.cache.delete(fingerprint);
|
|
414
|
+
this.staleRevalidatePromises.delete(fingerprint);
|
|
415
|
+
${options.enableDebugLogging
|
|
416
|
+
? "this.options.debugLogging && console.debug(`Cache invalidated: ${fingerprint}`);"
|
|
417
|
+
: ""}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Clear entire cache
|
|
422
|
+
*/
|
|
423
|
+
clear(): void {
|
|
424
|
+
this.cache.clear();
|
|
425
|
+
this.staleRevalidatePromises.clear();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get cache statistics
|
|
430
|
+
*/
|
|
431
|
+
getStats(): { size: number; hitRate: number } {
|
|
432
|
+
return {
|
|
433
|
+
size: this.cache.size(),
|
|
434
|
+
hitRate: 0 // Would need hit/miss tracking
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Revalidate stale entry in background
|
|
440
|
+
*/
|
|
441
|
+
revalidateInBackground<U extends T>(
|
|
442
|
+
fingerprint: string,
|
|
443
|
+
revalidator: () => Promise<U>
|
|
444
|
+
): void {
|
|
445
|
+
if (this.staleRevalidatePromises.has(fingerprint)) {
|
|
446
|
+
return; // Already revalidating
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const revalidationPromise = revalidator()
|
|
450
|
+
.then(freshData => {
|
|
451
|
+
this.set(fingerprint, freshData);
|
|
452
|
+
return freshData;
|
|
453
|
+
})
|
|
454
|
+
.catch(error => {
|
|
455
|
+
${options.enableDebugLogging
|
|
456
|
+
? "this.options.debugLogging && console.warn(`Background revalidation failed: ${fingerprint}`, error);"
|
|
457
|
+
: ""}
|
|
458
|
+
throw error;
|
|
459
|
+
})
|
|
460
|
+
.finally(() => {
|
|
461
|
+
this.staleRevalidatePromises.delete(fingerprint);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
this.staleRevalidatePromises.set(fingerprint, revalidationPromise);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Main request deduplicator
|
|
470
|
+
*/
|
|
471
|
+
export class RequestDeduplicator {
|
|
472
|
+
private readonly cacheManager: CacheManager;
|
|
473
|
+
private readonly coalescer: RequestCoalescer;
|
|
474
|
+
private readonly endpointPolicies: Record<string, any>;
|
|
475
|
+
|
|
476
|
+
constructor() {
|
|
477
|
+
this.cacheManager = new CacheManager({
|
|
478
|
+
defaultTTL: ${options.defaultTTL || 5 * 60 * 1000},
|
|
479
|
+
enableStaleWhileRevalidate: ${options.enableStaleWhileRevalidate || true},
|
|
480
|
+
debugLogging: ${options.enableDebugLogging || false}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
this.coalescer = new RequestCoalescer(${options.maxPendingRequests || 100});
|
|
484
|
+
this.endpointPolicies = ${JSON.stringify(options.endpointPolicies || {})};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Execute request with deduplication
|
|
489
|
+
*/
|
|
490
|
+
async execute<T>(
|
|
491
|
+
method: string,
|
|
492
|
+
url: string,
|
|
493
|
+
requestFactory: () => Promise<T>,
|
|
494
|
+
options: {
|
|
495
|
+
headers?: Record<string, string>;
|
|
496
|
+
body?: unknown;
|
|
497
|
+
params?: Record<string, unknown>;
|
|
498
|
+
skipCache?: boolean;
|
|
499
|
+
customTTL?: number;
|
|
500
|
+
} = {}
|
|
501
|
+
): Promise<T> {
|
|
502
|
+
if (options.skipCache) {
|
|
503
|
+
return requestFactory();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Generate request fingerprint
|
|
507
|
+
const fingerprint = RequestFingerprintGenerator.generate(
|
|
508
|
+
method,
|
|
509
|
+
url,
|
|
510
|
+
options.headers,
|
|
511
|
+
options.body,
|
|
512
|
+
options.params
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// Check endpoint-specific policy
|
|
516
|
+
const policy = this.getEndpointPolicy(url);
|
|
517
|
+
if (!policy.enabled) {
|
|
518
|
+
return requestFactory();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Try cache first
|
|
522
|
+
const cachedResult = this.cacheManager.get(fingerprint);
|
|
523
|
+
if (cachedResult !== undefined) {
|
|
524
|
+
// If stale and stale-while-revalidate is enabled, revalidate in background
|
|
525
|
+
if (policy.staleWhileRevalidate && !this.cacheManager.hasFresh(fingerprint)) {
|
|
526
|
+
this.cacheManager.revalidateInBackground(fingerprint, requestFactory);
|
|
527
|
+
}
|
|
528
|
+
return cachedResult as T;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// No cache hit, execute request with coalescing
|
|
532
|
+
const result = await this.coalescer.coalesce(fingerprint, requestFactory);
|
|
533
|
+
|
|
534
|
+
// Cache the result
|
|
535
|
+
const ttl = options.customTTL || policy.ttl || ${options.defaultTTL || 5 * 60 * 1000};
|
|
536
|
+
this.cacheManager.set(fingerprint, result, ttl);
|
|
537
|
+
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Invalidate cache for specific pattern
|
|
543
|
+
*/
|
|
544
|
+
invalidate(pattern: string | RegExp): void {
|
|
545
|
+
// In a real implementation, this would iterate through cache keys
|
|
546
|
+
// and invalidate matching entries
|
|
547
|
+
if (typeof pattern === 'string') {
|
|
548
|
+
const fingerprint = RequestFingerprintGenerator.generate('GET', pattern);
|
|
549
|
+
this.cacheManager.invalidate(fingerprint);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Clear all caches
|
|
555
|
+
*/
|
|
556
|
+
clearCache(): void {
|
|
557
|
+
this.cacheManager.clear();
|
|
558
|
+
this.coalescer.clear();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Get cache statistics
|
|
563
|
+
*/
|
|
564
|
+
getStats(): {
|
|
565
|
+
cacheSize: number;
|
|
566
|
+
pendingRequests: number;
|
|
567
|
+
cacheHitRate: number;
|
|
568
|
+
} {
|
|
569
|
+
return {
|
|
570
|
+
cacheSize: this.cacheManager.getStats().size,
|
|
571
|
+
pendingRequests: this.coalescer.getPendingCount(),
|
|
572
|
+
cacheHitRate: this.cacheManager.getStats().hitRate
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private getEndpointPolicy(url: string): {
|
|
577
|
+
enabled: boolean;
|
|
578
|
+
ttl: number;
|
|
579
|
+
staleWhileRevalidate: boolean;
|
|
580
|
+
} {
|
|
581
|
+
// Simple URL matching - in production, use more sophisticated matching
|
|
582
|
+
for (const [pattern, policy] of Object.entries(this.endpointPolicies)) {
|
|
583
|
+
if (url.includes(pattern)) {
|
|
584
|
+
return {
|
|
585
|
+
enabled: policy.enabled ?? true,
|
|
586
|
+
ttl: policy.ttl ?? ${options.defaultTTL || 5 * 60 * 1000},
|
|
587
|
+
staleWhileRevalidate: policy.staleWhileRevalidate ?? ${options.enableStaleWhileRevalidate || true}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
enabled: true,
|
|
594
|
+
ttl: ${options.defaultTTL || 5 * 60 * 1000},
|
|
595
|
+
staleWhileRevalidate: ${options.enableStaleWhileRevalidate || true}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Global deduplicator instance
|
|
602
|
+
*/
|
|
603
|
+
const globalDeduplicator = new RequestDeduplicator();
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Utility functions
|
|
607
|
+
*/
|
|
608
|
+
export function clearRequestCache(): void {
|
|
609
|
+
globalDeduplicator.clearCache();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function getCacheStats(): ReturnType<RequestDeduplicator['getStats']> {
|
|
613
|
+
return globalDeduplicator.getStats();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function invalidateCache(pattern: string | RegExp): void {
|
|
617
|
+
globalDeduplicator.invalidate(pattern);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Request wrapper with deduplication
|
|
622
|
+
*/
|
|
623
|
+
export function withDeduplication<T extends (...args: any[]) => Promise<any>>(
|
|
624
|
+
requestFn: T,
|
|
625
|
+
options: {
|
|
626
|
+
method: string;
|
|
627
|
+
urlExtractor: (...args: Parameters<T>) => string;
|
|
628
|
+
headersExtractor?: (...args: Parameters<T>) => Record<string, string>;
|
|
629
|
+
bodyExtractor?: (...args: Parameters<T>) => unknown;
|
|
630
|
+
paramsExtractor?: (...args: Parameters<T>) => Record<string, unknown>;
|
|
631
|
+
}
|
|
632
|
+
): T {
|
|
633
|
+
return ((...args: Parameters<T>) => {
|
|
634
|
+
const url = options.urlExtractor(...args);
|
|
635
|
+
const headers = options.headersExtractor?.(...args);
|
|
636
|
+
const body = options.bodyExtractor?.(...args);
|
|
637
|
+
const params = options.paramsExtractor?.(...args);
|
|
638
|
+
|
|
639
|
+
return globalDeduplicator.execute(
|
|
640
|
+
options.method,
|
|
641
|
+
url,
|
|
642
|
+
() => requestFn(...args),
|
|
643
|
+
{ headers, body, params }
|
|
644
|
+
);
|
|
645
|
+
}) as T;
|
|
646
|
+
}`;
|
|
647
|
+
}
|
|
648
|
+
// Export plugin for registration
|
|
649
|
+
exports.default = exports.requestDeduplicationPlugin;
|
|
650
|
+
//# sourceMappingURL=request-deduplication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-deduplication.js","sourceRoot":"","sources":["../../src/plugins/request-deduplication.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Zod Validation Plugin
|
|
3
|
+
*
|
|
4
|
+
* Generates Zod schemas from OpenAPI specifications for runtime validation.
|
|
5
|
+
* Implements sophisticated type-level validation mapping with performance optimizations.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* - Type-safe request/response validation pipelines
|
|
9
|
+
* - Performance-optimized validation caching
|
|
10
|
+
* - Branded type integration for compile-time safety
|
|
11
|
+
* - Runtime type guard generation
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* - Schema transformation with memoization
|
|
15
|
+
* - Lazy evaluation for expensive operations
|
|
16
|
+
* - Memory-efficient validation cache management
|
|
17
|
+
*/
|
|
18
|
+
import { SchemanticPlugin } from "../types/core";
|
|
19
|
+
type ZodType<T = unknown> = {
|
|
20
|
+
parse(data: unknown): T;
|
|
21
|
+
safeParse(data: unknown): {
|
|
22
|
+
success: true;
|
|
23
|
+
data: T;
|
|
24
|
+
} | {
|
|
25
|
+
success: false;
|
|
26
|
+
error: {
|
|
27
|
+
errors: Array<{
|
|
28
|
+
path: (string | number)[];
|
|
29
|
+
message: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
pipe<U>(schema: ZodType<U>): ZodType<U>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Advanced Zod Validation Plugin Implementation
|
|
37
|
+
*/
|
|
38
|
+
export declare const zodValidationPlugin: SchemanticPlugin;
|
|
39
|
+
export declare function memoizedValidation<T>(data: unknown, schema: ZodType<T>, cacheKey?: string, schemaVersion?: string): T;
|
|
40
|
+
/**
|
|
41
|
+
* Get performance statistics for the plugin
|
|
42
|
+
*/
|
|
43
|
+
export declare function getZodValidationPerformanceStats(): Record<string, {
|
|
44
|
+
avg: number;
|
|
45
|
+
min: number;
|
|
46
|
+
max: number;
|
|
47
|
+
count: number;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Clear performance metrics and caches
|
|
51
|
+
*/
|
|
52
|
+
export declare function clearZodValidationCaches(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Get validation cache statistics for monitoring
|
|
55
|
+
*/
|
|
56
|
+
export declare function getValidationCacheStats(): {
|
|
57
|
+
size: number;
|
|
58
|
+
maxSize: number;
|
|
59
|
+
hitRate?: number;
|
|
60
|
+
averageAccessTime?: number;
|
|
61
|
+
};
|
|
62
|
+
export default zodValidationPlugin;
|
|
63
|
+
//# sourceMappingURL=zod-validation.d.ts.map
|