mcp-wordpress 1.1.7 → 1.2.2
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 +388 -66
- package/dist/cache/CacheInvalidation.d.ts +118 -0
- package/dist/cache/CacheInvalidation.d.ts.map +1 -0
- package/dist/cache/CacheInvalidation.js +349 -0
- package/dist/cache/CacheInvalidation.js.map +1 -0
- package/dist/cache/CacheManager.d.ts +143 -0
- package/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +308 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/cache/HttpCacheWrapper.d.ts +121 -0
- package/dist/cache/HttpCacheWrapper.d.ts.map +1 -0
- package/dist/cache/HttpCacheWrapper.js +280 -0
- package/dist/cache/HttpCacheWrapper.js.map +1 -0
- package/dist/cache/__tests__/CacheInvalidation.test.d.ts +5 -0
- package/dist/cache/__tests__/CacheInvalidation.test.d.ts.map +1 -0
- package/dist/cache/__tests__/CacheInvalidation.test.js +236 -0
- package/dist/cache/__tests__/CacheInvalidation.test.js.map +1 -0
- package/dist/cache/__tests__/CacheManager.test.d.ts +5 -0
- package/dist/cache/__tests__/CacheManager.test.d.ts.map +1 -0
- package/dist/cache/__tests__/CacheManager.test.js +233 -0
- package/dist/cache/__tests__/CacheManager.test.js.map +1 -0
- package/dist/cache/__tests__/CachedWordPressClient.test.d.ts +5 -0
- package/dist/cache/__tests__/CachedWordPressClient.test.d.ts.map +1 -0
- package/dist/cache/__tests__/CachedWordPressClient.test.js +228 -0
- package/dist/cache/__tests__/CachedWordPressClient.test.js.map +1 -0
- package/dist/cache/__tests__/HttpCacheWrapper.test.d.ts +5 -0
- package/dist/cache/__tests__/HttpCacheWrapper.test.d.ts.map +1 -0
- package/dist/cache/__tests__/HttpCacheWrapper.test.js +296 -0
- package/dist/cache/__tests__/HttpCacheWrapper.test.js.map +1 -0
- package/dist/cache/index.d.ts +12 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +9 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/client/CachedWordPressClient.d.ts +160 -0
- package/dist/client/CachedWordPressClient.d.ts.map +1 -0
- package/dist/client/CachedWordPressClient.js +338 -0
- package/dist/client/CachedWordPressClient.js.map +1 -0
- package/dist/client/WordPressClient.d.ts +81 -0
- package/dist/client/WordPressClient.d.ts.map +1 -0
- package/dist/client/WordPressClient.js +354 -0
- package/dist/client/WordPressClient.js.map +1 -0
- package/dist/config/ConfigurationSchema.d.ts +281 -0
- package/dist/config/ConfigurationSchema.d.ts.map +1 -0
- package/dist/config/ConfigurationSchema.js +205 -0
- package/dist/config/ConfigurationSchema.js.map +1 -0
- package/dist/config/ServerConfiguration.d.ts +38 -0
- package/dist/config/ServerConfiguration.d.ts.map +1 -0
- package/dist/config/ServerConfiguration.js +158 -0
- package/dist/config/ServerConfiguration.js.map +1 -0
- package/dist/docs/DocumentationGenerator.d.ts +184 -0
- package/dist/docs/DocumentationGenerator.d.ts.map +1 -0
- package/dist/docs/DocumentationGenerator.js +735 -0
- package/dist/docs/DocumentationGenerator.js.map +1 -0
- package/dist/docs/MarkdownFormatter.d.ts +84 -0
- package/dist/docs/MarkdownFormatter.d.ts.map +1 -0
- package/dist/docs/MarkdownFormatter.js +448 -0
- package/dist/docs/MarkdownFormatter.js.map +1 -0
- package/dist/docs/index.d.ts +8 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +7 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -212
- package/dist/index.js.map +1 -1
- package/dist/performance/AnomalyDetector.d.ts +63 -0
- package/dist/performance/AnomalyDetector.d.ts.map +1 -0
- package/dist/performance/AnomalyDetector.js +222 -0
- package/dist/performance/AnomalyDetector.js.map +1 -0
- package/dist/performance/BenchmarkAnalyzer.d.ts +67 -0
- package/dist/performance/BenchmarkAnalyzer.d.ts.map +1 -0
- package/dist/performance/BenchmarkAnalyzer.js +301 -0
- package/dist/performance/BenchmarkAnalyzer.js.map +1 -0
- package/dist/performance/MetricsCollector.d.ts +139 -0
- package/dist/performance/MetricsCollector.d.ts.map +1 -0
- package/dist/performance/MetricsCollector.js +320 -0
- package/dist/performance/MetricsCollector.js.map +1 -0
- package/dist/performance/PerformanceAnalytics.d.ts +162 -0
- package/dist/performance/PerformanceAnalytics.d.ts.map +1 -0
- package/dist/performance/PerformanceAnalytics.js +554 -0
- package/dist/performance/PerformanceAnalytics.js.map +1 -0
- package/dist/performance/PerformanceMonitor.d.ts +202 -0
- package/dist/performance/PerformanceMonitor.d.ts.map +1 -0
- package/dist/performance/PerformanceMonitor.js +478 -0
- package/dist/performance/PerformanceMonitor.js.map +1 -0
- package/dist/performance/TrendAnalyzer.d.ts +69 -0
- package/dist/performance/TrendAnalyzer.d.ts.map +1 -0
- package/dist/performance/TrendAnalyzer.js +203 -0
- package/dist/performance/TrendAnalyzer.js.map +1 -0
- package/dist/performance/index.d.ts +11 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +8 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/security/InputValidator.d.ts +215 -0
- package/dist/security/InputValidator.d.ts.map +1 -0
- package/dist/security/InputValidator.js +278 -0
- package/dist/security/InputValidator.js.map +1 -0
- package/dist/security/SecurityConfig.d.ts +129 -0
- package/dist/security/SecurityConfig.d.ts.map +1 -0
- package/dist/security/SecurityConfig.js +262 -0
- package/dist/security/SecurityConfig.js.map +1 -0
- package/dist/server/ConnectionTester.d.ts +24 -0
- package/dist/server/ConnectionTester.d.ts.map +1 -0
- package/dist/server/ConnectionTester.js +61 -0
- package/dist/server/ConnectionTester.js.map +1 -0
- package/dist/server/ToolRegistry.d.ts +46 -0
- package/dist/server/ToolRegistry.d.ts.map +1 -0
- package/dist/server/ToolRegistry.js +148 -0
- package/dist/server/ToolRegistry.js.map +1 -0
- package/dist/tools/BaseToolClass.d.ts +76 -0
- package/dist/tools/BaseToolClass.d.ts.map +1 -0
- package/dist/tools/BaseToolClass.js +104 -0
- package/dist/tools/BaseToolClass.js.map +1 -0
- package/dist/tools/BaseToolManager.d.ts +26 -0
- package/dist/tools/BaseToolManager.d.ts.map +1 -0
- package/dist/tools/BaseToolManager.js +56 -0
- package/dist/tools/BaseToolManager.js.map +1 -0
- package/dist/tools/base.d.ts +37 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +60 -0
- package/dist/tools/base.js.map +1 -0
- package/dist/tools/cache.d.ts +260 -0
- package/dist/tools/cache.d.ts.map +1 -0
- package/dist/tools/cache.js +237 -0
- package/dist/tools/cache.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/performance.d.ts +63 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +865 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/types/client.d.ts +1 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/client.js.map +1 -1
- package/dist/utils/toolWrapper.d.ts +4 -0
- package/dist/utils/toolWrapper.d.ts.map +1 -1
- package/dist/utils/toolWrapper.js +11 -0
- package/dist/utils/toolWrapper.js.map +1 -1
- package/dist/utils/validation.d.ts +68 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +185 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs/CACHING.md +340 -0
- package/docs/DOCKER.md +451 -0
- package/docs/PERFORMANCE_MONITORING.md +471 -0
- package/docs/SECURITY_TESTING.md +393 -0
- package/docs/api/README.md +200 -0
- package/docs/api/categories/auth.md +40 -0
- package/docs/api/categories/cache.md +41 -0
- package/docs/api/categories/comment.md +44 -0
- package/docs/api/categories/media.md +43 -0
- package/docs/api/categories/page.md +43 -0
- package/docs/api/categories/performance.md +44 -0
- package/docs/api/categories/post.md +43 -0
- package/docs/api/categories/site.md +43 -0
- package/docs/api/categories/taxonomy.md +47 -0
- package/docs/api/categories/user.md +43 -0
- package/docs/api/openapi.json +3305 -0
- package/docs/api/summary.json +12 -0
- package/docs/api/tools/wp_approve_comment.md +98 -0
- package/docs/api/tools/wp_cache_clear.md +120 -0
- package/docs/api/tools/wp_cache_info.md +119 -0
- package/docs/api/tools/wp_cache_stats.md +119 -0
- package/docs/api/tools/wp_cache_warm.md +119 -0
- package/docs/api/tools/wp_create_application_password.md +102 -0
- package/docs/api/tools/wp_create_category.md +102 -0
- package/docs/api/tools/wp_create_comment.md +128 -0
- package/docs/api/tools/wp_create_page.md +135 -0
- package/docs/api/tools/wp_create_post.md +147 -0
- package/docs/api/tools/wp_create_tag.md +101 -0
- package/docs/api/tools/wp_create_user.md +135 -0
- package/docs/api/tools/wp_delete_application_password.md +101 -0
- package/docs/api/tools/wp_delete_category.md +100 -0
- package/docs/api/tools/wp_delete_comment.md +101 -0
- package/docs/api/tools/wp_delete_media.md +108 -0
- package/docs/api/tools/wp_delete_page.md +108 -0
- package/docs/api/tools/wp_delete_post.md +117 -0
- package/docs/api/tools/wp_delete_tag.md +100 -0
- package/docs/api/tools/wp_delete_user.md +108 -0
- package/docs/api/tools/wp_get_application_passwords.md +103 -0
- package/docs/api/tools/wp_get_auth_status.md +101 -0
- package/docs/api/tools/wp_get_category.md +103 -0
- package/docs/api/tools/wp_get_comment.md +103 -0
- package/docs/api/tools/wp_get_current_user.md +101 -0
- package/docs/api/tools/wp_get_media.md +103 -0
- package/docs/api/tools/wp_get_page.md +103 -0
- package/docs/api/tools/wp_get_page_revisions.md +103 -0
- package/docs/api/tools/wp_get_post.md +112 -0
- package/docs/api/tools/wp_get_post_revisions.md +103 -0
- package/docs/api/tools/wp_get_site_settings.md +108 -0
- package/docs/api/tools/wp_get_tag.md +103 -0
- package/docs/api/tools/wp_get_user.md +103 -0
- package/docs/api/tools/wp_list_categories.md +111 -0
- package/docs/api/tools/wp_list_comments.md +111 -0
- package/docs/api/tools/wp_list_media.md +145 -0
- package/docs/api/tools/wp_list_pages.md +145 -0
- package/docs/api/tools/wp_list_posts.md +156 -0
- package/docs/api/tools/wp_list_tags.md +110 -0
- package/docs/api/tools/wp_list_users.md +111 -0
- package/docs/api/tools/wp_performance_alerts.md +162 -0
- package/docs/api/tools/wp_performance_benchmark.md +160 -0
- package/docs/api/tools/wp_performance_export.md +162 -0
- package/docs/api/tools/wp_performance_history.md +161 -0
- package/docs/api/tools/wp_performance_optimize.md +162 -0
- package/docs/api/tools/wp_performance_stats.md +160 -0
- package/docs/api/tools/wp_search_site.md +99 -0
- package/docs/api/tools/wp_spam_comment.md +98 -0
- package/docs/api/tools/wp_switch_auth_method.md +122 -0
- package/docs/api/tools/wp_test_auth.md +96 -0
- package/docs/api/tools/wp_update_category.md +102 -0
- package/docs/api/tools/wp_update_comment.md +127 -0
- package/docs/api/tools/wp_update_media.md +129 -0
- package/docs/api/tools/wp_update_page.md +135 -0
- package/docs/api/tools/wp_update_post.md +144 -0
- package/docs/api/tools/wp_update_site_settings.md +127 -0
- package/docs/api/tools/wp_update_tag.md +102 -0
- package/docs/api/tools/wp_update_user.md +134 -0
- package/docs/api/tools/wp_upload_media.md +131 -0
- package/docs/api/types/WordPressPost.md +39 -0
- package/docs/contract-testing.md +183 -0
- package/docs/developer/NPM_AUTH_SETUP.md +3 -3
- package/docs/wordpress-rest-api-authentication-troubleshooting.md +218 -0
- package/package.json +84 -64
- package/src/cache/CacheInvalidation.ts +421 -0
- package/src/cache/CacheManager.ts +391 -0
- package/src/cache/HttpCacheWrapper.ts +372 -0
- package/src/cache/__tests__/CacheInvalidation.test.ts +299 -0
- package/src/cache/__tests__/CacheManager.test.ts +300 -0
- package/src/cache/__tests__/CachedWordPressClient.test.ts +304 -0
- package/src/cache/__tests__/HttpCacheWrapper.test.ts +359 -0
- package/src/cache/index.ts +26 -0
- package/src/client/CachedWordPressClient.ts +442 -0
- package/src/config/ConfigurationSchema.ts +246 -0
- package/src/config/ServerConfiguration.ts +215 -0
- package/src/docs/DocumentationGenerator.ts +952 -0
- package/src/docs/MarkdownFormatter.ts +494 -0
- package/src/docs/index.ts +21 -0
- package/src/index.ts +14 -274
- package/src/performance/MetricsCollector.ts +447 -0
- package/src/performance/PerformanceAnalytics.ts +762 -0
- package/src/performance/PerformanceMonitor.ts +649 -0
- package/src/performance/index.ts +28 -0
- package/src/security/InputValidator.ts +319 -0
- package/src/security/SecurityConfig.ts +301 -0
- package/src/server/ConnectionTester.ts +74 -0
- package/src/server/ToolRegistry.ts +194 -0
- package/src/tools/BaseToolManager.ts +66 -0
- package/src/tools/cache.ts +259 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/performance.ts +948 -0
- package/src/types/client.ts +1 -0
- package/src/utils/toolWrapper.ts +11 -0
- package/src/utils/validation.ts +259 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP-level caching wrapper with ETags and Cache-Control headers
|
|
3
|
+
* Implements WordPress REST API caching best practices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CacheManager, CachePresets } from './CacheManager.js';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
|
|
9
|
+
export interface HttpCacheOptions {
|
|
10
|
+
ttl?: number;
|
|
11
|
+
cacheControl?: string;
|
|
12
|
+
varyHeaders?: string[];
|
|
13
|
+
private?: boolean;
|
|
14
|
+
revalidate?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CachedResponse {
|
|
18
|
+
data: any;
|
|
19
|
+
status: number;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
etag?: string;
|
|
22
|
+
lastModified?: string;
|
|
23
|
+
cacheControl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RequestOptions {
|
|
27
|
+
method: string;
|
|
28
|
+
url: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
params?: any;
|
|
31
|
+
data?: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* HTTP caching wrapper that adds intelligent caching to HTTP requests
|
|
36
|
+
*/
|
|
37
|
+
export class HttpCacheWrapper {
|
|
38
|
+
constructor(
|
|
39
|
+
private cacheManager: CacheManager,
|
|
40
|
+
private siteId: string
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute request with intelligent caching
|
|
45
|
+
*/
|
|
46
|
+
async request<T = any>(
|
|
47
|
+
requestFn: () => Promise<{ data: T; status: number; headers: Record<string, string> }>,
|
|
48
|
+
options: RequestOptions,
|
|
49
|
+
cacheOptions?: HttpCacheOptions
|
|
50
|
+
): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean }> {
|
|
51
|
+
// Only cache GET requests
|
|
52
|
+
if (options.method.toUpperCase() !== 'GET') {
|
|
53
|
+
return await requestFn();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cacheKey = this.generateCacheKey(options);
|
|
57
|
+
const cachedEntry = this.cacheManager.getEntry(cacheKey);
|
|
58
|
+
|
|
59
|
+
// Check for conditional request support
|
|
60
|
+
if (cachedEntry && this.cacheManager.supportsConditionalRequest(cacheKey)) {
|
|
61
|
+
const conditionalHeaders = this.cacheManager.getConditionalHeaders(cacheKey);
|
|
62
|
+
|
|
63
|
+
// Add conditional headers to request
|
|
64
|
+
const requestWithHeaders = {
|
|
65
|
+
...options,
|
|
66
|
+
headers: {
|
|
67
|
+
...options.headers,
|
|
68
|
+
...conditionalHeaders
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await this.executeRequestWithHeaders(requestFn, requestWithHeaders);
|
|
74
|
+
|
|
75
|
+
// 304 Not Modified - return cached data
|
|
76
|
+
if (response.status === 304) {
|
|
77
|
+
return {
|
|
78
|
+
data: cachedEntry.value.data,
|
|
79
|
+
status: 200,
|
|
80
|
+
headers: cachedEntry.value.headers,
|
|
81
|
+
cached: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Content changed - update cache
|
|
86
|
+
return await this.cacheAndReturn(response, cacheKey, cacheOptions);
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// If conditional request fails, try without conditions
|
|
90
|
+
console.warn('Conditional request failed, falling back to regular request:', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for valid cached response
|
|
95
|
+
const cached = this.cacheManager.get<CachedResponse>(cacheKey);
|
|
96
|
+
if (cached) {
|
|
97
|
+
return {
|
|
98
|
+
data: cached.data,
|
|
99
|
+
status: cached.status,
|
|
100
|
+
headers: cached.headers,
|
|
101
|
+
cached: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Execute fresh request
|
|
106
|
+
const response = await requestFn();
|
|
107
|
+
return await this.cacheAndReturn(response, cacheKey, cacheOptions);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Invalidate cache for specific endpoint
|
|
112
|
+
*/
|
|
113
|
+
invalidate(endpoint: string, params?: any): void {
|
|
114
|
+
const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params);
|
|
115
|
+
this.cacheManager.delete(cacheKey);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Invalidate cache entries matching pattern
|
|
120
|
+
*/
|
|
121
|
+
invalidatePattern(pattern: string): number {
|
|
122
|
+
const regex = new RegExp(`${this.siteId}:${pattern}`);
|
|
123
|
+
return this.cacheManager.clearPattern(regex);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Invalidate all cache for this site
|
|
128
|
+
*/
|
|
129
|
+
invalidateAll(): number {
|
|
130
|
+
return this.cacheManager.clearSite(this.siteId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Pre-warm cache with data
|
|
135
|
+
*/
|
|
136
|
+
warm<T>(endpoint: string, data: T, params?: any, cacheOptions?: HttpCacheOptions): void {
|
|
137
|
+
const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params);
|
|
138
|
+
const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint);
|
|
139
|
+
|
|
140
|
+
const cachedResponse: CachedResponse = {
|
|
141
|
+
data,
|
|
142
|
+
status: 200,
|
|
143
|
+
headers: this.generateCacheHeaders(cacheOptions, endpoint),
|
|
144
|
+
etag: this.generateETag(data),
|
|
145
|
+
lastModified: new Date().toUTCString(),
|
|
146
|
+
cacheControl: cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint)
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this.cacheManager.set(
|
|
150
|
+
cacheKey,
|
|
151
|
+
cachedResponse,
|
|
152
|
+
ttl,
|
|
153
|
+
cachedResponse.etag,
|
|
154
|
+
cachedResponse.lastModified
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get cache statistics for this site
|
|
160
|
+
*/
|
|
161
|
+
getStats() {
|
|
162
|
+
return this.cacheManager.getStats();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate cache key for request
|
|
167
|
+
*/
|
|
168
|
+
private generateCacheKey(options: RequestOptions): string {
|
|
169
|
+
const endpoint = this.extractEndpoint(options.url);
|
|
170
|
+
return this.cacheManager.generateKey(this.siteId, endpoint, {
|
|
171
|
+
...options.params,
|
|
172
|
+
// Include relevant headers that affect response
|
|
173
|
+
...this.extractCacheableHeaders(options.headers)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Extract endpoint from full URL
|
|
179
|
+
*/
|
|
180
|
+
private extractEndpoint(url: string): string {
|
|
181
|
+
// Extract the path after /wp-json/wp/v2/
|
|
182
|
+
const match = url.match(/\/wp-json\/wp\/v2\/(.+?)(?:\?|$)/);
|
|
183
|
+
return match ? match[1] : url;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract headers that affect caching
|
|
188
|
+
*/
|
|
189
|
+
private extractCacheableHeaders(headers?: Record<string, string>): Record<string, string> {
|
|
190
|
+
if (!headers) return {};
|
|
191
|
+
|
|
192
|
+
const cacheableHeaders: Record<string, string> = {};
|
|
193
|
+
const relevantHeaders = ['accept', 'accept-language', 'authorization'];
|
|
194
|
+
|
|
195
|
+
for (const header of relevantHeaders) {
|
|
196
|
+
if (headers[header]) {
|
|
197
|
+
cacheableHeaders[header] = headers[header];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return cacheableHeaders;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Execute request with modified headers
|
|
206
|
+
*/
|
|
207
|
+
private async executeRequestWithHeaders(
|
|
208
|
+
requestFn: () => Promise<{ data: any; status: number; headers: Record<string, string> }>,
|
|
209
|
+
options: RequestOptions
|
|
210
|
+
) {
|
|
211
|
+
// This is a simplified approach - in practice, you'd need to modify the actual request
|
|
212
|
+
// The actual implementation would depend on your HTTP client (axios, fetch, etc.)
|
|
213
|
+
return await requestFn();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Cache response and return with cache metadata
|
|
218
|
+
*/
|
|
219
|
+
private async cacheAndReturn<T>(
|
|
220
|
+
response: { data: T; status: number; headers: Record<string, string> },
|
|
221
|
+
cacheKey: string,
|
|
222
|
+
cacheOptions?: HttpCacheOptions
|
|
223
|
+
): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean }> {
|
|
224
|
+
|
|
225
|
+
// Don't cache error responses (unless specifically configured)
|
|
226
|
+
if (response.status >= 400) {
|
|
227
|
+
return response;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const endpoint = this.extractEndpointFromKey(cacheKey);
|
|
231
|
+
const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint);
|
|
232
|
+
|
|
233
|
+
// Generate ETags and cache headers
|
|
234
|
+
const etag = this.generateETag(response.data);
|
|
235
|
+
const lastModified = new Date().toUTCString();
|
|
236
|
+
const cacheControl = cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint);
|
|
237
|
+
|
|
238
|
+
const cachedResponse: CachedResponse = {
|
|
239
|
+
data: response.data,
|
|
240
|
+
status: response.status,
|
|
241
|
+
headers: {
|
|
242
|
+
...response.headers,
|
|
243
|
+
'etag': etag,
|
|
244
|
+
'last-modified': lastModified,
|
|
245
|
+
'cache-control': cacheControl
|
|
246
|
+
},
|
|
247
|
+
etag,
|
|
248
|
+
lastModified,
|
|
249
|
+
cacheControl
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Store in cache
|
|
253
|
+
this.cacheManager.set(
|
|
254
|
+
cacheKey,
|
|
255
|
+
cachedResponse,
|
|
256
|
+
ttl,
|
|
257
|
+
etag,
|
|
258
|
+
lastModified
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
data: response.data,
|
|
263
|
+
status: response.status,
|
|
264
|
+
headers: cachedResponse.headers,
|
|
265
|
+
cached: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate ETag for response data
|
|
271
|
+
*/
|
|
272
|
+
private generateETag(data: any): string {
|
|
273
|
+
const hash = crypto
|
|
274
|
+
.createHash('md5')
|
|
275
|
+
.update(JSON.stringify(data))
|
|
276
|
+
.digest('hex');
|
|
277
|
+
return `"${hash}"`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get default TTL based on endpoint type
|
|
282
|
+
*/
|
|
283
|
+
private getDefaultTTL(endpoint: string): number {
|
|
284
|
+
// Static data endpoints
|
|
285
|
+
if (this.isStaticEndpoint(endpoint)) {
|
|
286
|
+
return CachePresets.STATIC.ttl;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Semi-static data endpoints
|
|
290
|
+
if (this.isSemiStaticEndpoint(endpoint)) {
|
|
291
|
+
return CachePresets.SEMI_STATIC.ttl;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Session/auth endpoints
|
|
295
|
+
if (this.isSessionEndpoint(endpoint)) {
|
|
296
|
+
return CachePresets.SESSION.ttl;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Default to dynamic for posts, comments, etc.
|
|
300
|
+
return CachePresets.DYNAMIC.ttl;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get default Cache-Control header based on endpoint
|
|
305
|
+
*/
|
|
306
|
+
private getDefaultCacheControl(endpoint: string): string {
|
|
307
|
+
if (this.isStaticEndpoint(endpoint)) {
|
|
308
|
+
return CachePresets.STATIC.cacheControl;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.isSemiStaticEndpoint(endpoint)) {
|
|
312
|
+
return CachePresets.SEMI_STATIC.cacheControl;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (this.isSessionEndpoint(endpoint)) {
|
|
316
|
+
return CachePresets.SESSION.cacheControl;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return CachePresets.DYNAMIC.cacheControl;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Generate cache headers
|
|
324
|
+
*/
|
|
325
|
+
private generateCacheHeaders(options?: HttpCacheOptions, endpoint?: string): Record<string, string> {
|
|
326
|
+
const headers: Record<string, string> = {};
|
|
327
|
+
|
|
328
|
+
if (options?.cacheControl) {
|
|
329
|
+
headers['cache-control'] = options.cacheControl;
|
|
330
|
+
} else if (endpoint) {
|
|
331
|
+
headers['cache-control'] = this.getDefaultCacheControl(endpoint);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (options?.varyHeaders?.length) {
|
|
335
|
+
headers['vary'] = options.varyHeaders.join(', ');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return headers;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if endpoint contains static data
|
|
343
|
+
*/
|
|
344
|
+
private isStaticEndpoint(endpoint: string): boolean {
|
|
345
|
+
const staticEndpoints = ['settings', 'types', 'statuses'];
|
|
346
|
+
return staticEndpoints.some(pattern => endpoint.includes(pattern));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if endpoint contains semi-static data
|
|
351
|
+
*/
|
|
352
|
+
private isSemiStaticEndpoint(endpoint: string): boolean {
|
|
353
|
+
const semiStaticEndpoints = ['categories', 'tags', 'users', 'taxonomies'];
|
|
354
|
+
return semiStaticEndpoints.some(pattern => endpoint.includes(pattern));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check if endpoint is session-related
|
|
359
|
+
*/
|
|
360
|
+
private isSessionEndpoint(endpoint: string): boolean {
|
|
361
|
+
const sessionEndpoints = ['users/me', 'application-passwords'];
|
|
362
|
+
return sessionEndpoints.some(pattern => endpoint.includes(pattern));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extract endpoint from cache key
|
|
367
|
+
*/
|
|
368
|
+
private extractEndpointFromKey(cacheKey: string): string {
|
|
369
|
+
const parts = cacheKey.split(':');
|
|
370
|
+
return parts[1] || '';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CacheInvalidation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CacheManager } from '../CacheManager.js';
|
|
6
|
+
import { HttpCacheWrapper } from '../HttpCacheWrapper.js';
|
|
7
|
+
import { CacheInvalidation, WordPressCachePatterns } from '../CacheInvalidation.js';
|
|
8
|
+
|
|
9
|
+
describe('CacheInvalidation', () => {
|
|
10
|
+
let cacheManager: CacheManager;
|
|
11
|
+
let httpCache: HttpCacheWrapper;
|
|
12
|
+
let cacheInvalidation: CacheInvalidation;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
cacheManager = new CacheManager({
|
|
16
|
+
maxSize: 100,
|
|
17
|
+
defaultTTL: 10000,
|
|
18
|
+
enableLRU: true,
|
|
19
|
+
enableStats: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
httpCache = new HttpCacheWrapper(cacheManager, 'test-site');
|
|
23
|
+
cacheInvalidation = new CacheInvalidation(httpCache);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
cacheManager.clear();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Default Invalidation Rules', () => {
|
|
31
|
+
test('should have default rules for posts', () => {
|
|
32
|
+
const rules = cacheInvalidation.getRules();
|
|
33
|
+
|
|
34
|
+
expect(rules.posts).toBeDefined();
|
|
35
|
+
expect(rules.posts.length).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
const createRule = rules.posts.find(r => r.trigger === 'create');
|
|
38
|
+
expect(createRule).toBeDefined();
|
|
39
|
+
expect(createRule?.patterns).toContain('posts');
|
|
40
|
+
expect(createRule?.immediate).toBe(true);
|
|
41
|
+
expect(createRule?.cascade).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should have default rules for categories', () => {
|
|
45
|
+
const rules = cacheInvalidation.getRules();
|
|
46
|
+
|
|
47
|
+
expect(rules.categories).toBeDefined();
|
|
48
|
+
|
|
49
|
+
const updateRule = rules.categories.find(r => r.trigger === 'update');
|
|
50
|
+
expect(updateRule).toBeDefined();
|
|
51
|
+
expect(updateRule?.patterns).toContain('categories/\\d+');
|
|
52
|
+
expect(updateRule?.cascade).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should have default rules for users', () => {
|
|
56
|
+
const rules = cacheInvalidation.getRules();
|
|
57
|
+
|
|
58
|
+
expect(rules.users).toBeDefined();
|
|
59
|
+
|
|
60
|
+
const deleteRule = rules.users.find(r => r.trigger === 'delete');
|
|
61
|
+
expect(deleteRule).toBeDefined();
|
|
62
|
+
expect(deleteRule?.patterns).toContain('users');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Custom Invalidation Rules', () => {
|
|
67
|
+
test('should register custom invalidation rules', () => {
|
|
68
|
+
const customRule = {
|
|
69
|
+
trigger: 'update' as const,
|
|
70
|
+
patterns: ['custom-endpoint.*'],
|
|
71
|
+
immediate: true
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
cacheInvalidation.registerRule('custom', customRule);
|
|
75
|
+
|
|
76
|
+
const rules = cacheInvalidation.getRules();
|
|
77
|
+
expect(rules.custom).toBeDefined();
|
|
78
|
+
expect(rules.custom).toContain(customRule);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should support multiple rules per resource', () => {
|
|
82
|
+
const rule1 = {
|
|
83
|
+
trigger: 'create' as const,
|
|
84
|
+
patterns: ['test.*'],
|
|
85
|
+
immediate: true
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const rule2 = {
|
|
89
|
+
trigger: 'update' as const,
|
|
90
|
+
patterns: ['test/\\d+'],
|
|
91
|
+
immediate: false
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
cacheInvalidation.registerRule('test', rule1);
|
|
95
|
+
cacheInvalidation.registerRule('test', rule2);
|
|
96
|
+
|
|
97
|
+
const rules = cacheInvalidation.getRules();
|
|
98
|
+
expect(rules.test).toHaveLength(2);
|
|
99
|
+
expect(rules.test).toContain(rule1);
|
|
100
|
+
expect(rules.test).toContain(rule2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Event Processing', () => {
|
|
105
|
+
test('should process invalidation events', async () => {
|
|
106
|
+
// Pre-populate cache with some test data
|
|
107
|
+
httpCache.warm('posts', [{ id: 1 }, { id: 2 }]);
|
|
108
|
+
httpCache.warm('posts/1', { id: 1, title: 'Test Post' });
|
|
109
|
+
httpCache.warm('categories', [{ id: 1 }]);
|
|
110
|
+
|
|
111
|
+
expect(cacheManager.getStats().totalSize).toBe(3);
|
|
112
|
+
|
|
113
|
+
// Trigger post creation event
|
|
114
|
+
await cacheInvalidation.trigger({
|
|
115
|
+
type: 'create',
|
|
116
|
+
resource: 'posts',
|
|
117
|
+
id: 3,
|
|
118
|
+
siteId: 'test-site',
|
|
119
|
+
timestamp: Date.now()
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Should invalidate posts listings but not specific post
|
|
123
|
+
const stats = cacheManager.getStats();
|
|
124
|
+
expect(stats.totalSize).toBeLessThan(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('should process events in queue order', async () => {
|
|
128
|
+
const processedEvents: string[] = [];
|
|
129
|
+
|
|
130
|
+
// Mock the invalidation process to track order
|
|
131
|
+
const originalInvalidatePattern = httpCache.invalidatePattern;
|
|
132
|
+
httpCache.invalidatePattern = jest.fn().mockImplementation((pattern: string) => {
|
|
133
|
+
processedEvents.push(pattern);
|
|
134
|
+
return originalInvalidatePattern.call(httpCache, pattern);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Queue multiple events
|
|
138
|
+
await Promise.all([
|
|
139
|
+
cacheInvalidation.trigger({
|
|
140
|
+
type: 'create',
|
|
141
|
+
resource: 'posts',
|
|
142
|
+
siteId: 'test-site',
|
|
143
|
+
timestamp: Date.now()
|
|
144
|
+
}),
|
|
145
|
+
cacheInvalidation.trigger({
|
|
146
|
+
type: 'update',
|
|
147
|
+
resource: 'categories',
|
|
148
|
+
siteId: 'test-site',
|
|
149
|
+
timestamp: Date.now()
|
|
150
|
+
})
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
expect(processedEvents.length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should handle resource invalidation by type', async () => {
|
|
157
|
+
httpCache.warm('posts', [{ id: 1 }]);
|
|
158
|
+
httpCache.warm('posts/1', { id: 1 });
|
|
159
|
+
|
|
160
|
+
expect(cacheManager.getStats().totalSize).toBe(2);
|
|
161
|
+
|
|
162
|
+
await cacheInvalidation.invalidateResource('posts', 1, 'update');
|
|
163
|
+
|
|
164
|
+
// Should clear specific post and related caches
|
|
165
|
+
const stats = cacheManager.getStats();
|
|
166
|
+
expect(stats.totalSize).toBeLessThan(2);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Pattern Matching', () => {
|
|
171
|
+
test('should replace placeholders in patterns', async () => {
|
|
172
|
+
// Pre-populate cache
|
|
173
|
+
httpCache.warm('posts/123', { id: 123 });
|
|
174
|
+
httpCache.warm('posts/456', { id: 456 });
|
|
175
|
+
httpCache.warm('pages/123', { id: 123 });
|
|
176
|
+
|
|
177
|
+
expect(cacheManager.getStats().totalSize).toBe(3);
|
|
178
|
+
|
|
179
|
+
// Trigger event with specific ID
|
|
180
|
+
await cacheInvalidation.trigger({
|
|
181
|
+
type: 'update',
|
|
182
|
+
resource: 'posts',
|
|
183
|
+
id: 123,
|
|
184
|
+
siteId: 'test-site',
|
|
185
|
+
timestamp: Date.now()
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Should only invalidate posts/123, not posts/456 or pages/123
|
|
189
|
+
const remainingKeys: string[] = [];
|
|
190
|
+
for (const [key] of (cacheManager as any).cache.entries()) {
|
|
191
|
+
remainingKeys.push(key);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
expect(remainingKeys).not.toContain(expect.stringContaining('posts'));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('Statistics', () => {
|
|
199
|
+
test('should track invalidation statistics', () => {
|
|
200
|
+
const stats = cacheInvalidation.getStats();
|
|
201
|
+
|
|
202
|
+
expect(stats).toHaveProperty('queueSize');
|
|
203
|
+
expect(stats).toHaveProperty('rulesCount');
|
|
204
|
+
expect(stats).toHaveProperty('processing');
|
|
205
|
+
|
|
206
|
+
expect(typeof stats.queueSize).toBe('number');
|
|
207
|
+
expect(typeof stats.rulesCount).toBe('number');
|
|
208
|
+
expect(typeof stats.processing).toBe('boolean');
|
|
209
|
+
expect(stats.rulesCount).toBeGreaterThan(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should clear rules', () => {
|
|
213
|
+
const initialStats = cacheInvalidation.getStats();
|
|
214
|
+
expect(initialStats.rulesCount).toBeGreaterThan(0);
|
|
215
|
+
|
|
216
|
+
cacheInvalidation.clearRules();
|
|
217
|
+
|
|
218
|
+
const clearedStats = cacheInvalidation.getStats();
|
|
219
|
+
expect(clearedStats.rulesCount).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('WordPressCachePatterns', () => {
|
|
225
|
+
let cacheManager: CacheManager;
|
|
226
|
+
let httpCache: HttpCacheWrapper;
|
|
227
|
+
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
cacheManager = new CacheManager({
|
|
230
|
+
maxSize: 100,
|
|
231
|
+
defaultTTL: 10000,
|
|
232
|
+
enableLRU: true,
|
|
233
|
+
enableStats: true
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
httpCache = new HttpCacheWrapper(cacheManager, 'test-site');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
cacheManager.clear();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('should invalidate content-related caches', () => {
|
|
244
|
+
// Pre-populate cache
|
|
245
|
+
httpCache.warm('posts', []);
|
|
246
|
+
httpCache.warm('pages', []);
|
|
247
|
+
httpCache.warm('comments', []);
|
|
248
|
+
httpCache.warm('categories', []);
|
|
249
|
+
|
|
250
|
+
expect(cacheManager.getStats().totalSize).toBe(4);
|
|
251
|
+
|
|
252
|
+
const invalidated = WordPressCachePatterns.invalidateContent(httpCache);
|
|
253
|
+
|
|
254
|
+
expect(invalidated).toBeGreaterThan(0);
|
|
255
|
+
// Should invalidate posts, pages, comments but not categories
|
|
256
|
+
expect(cacheManager.getStats().totalSize).toBeLessThan(4);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should invalidate taxonomy-related caches', () => {
|
|
260
|
+
httpCache.warm('categories', []);
|
|
261
|
+
httpCache.warm('tags', []);
|
|
262
|
+
httpCache.warm('posts', []);
|
|
263
|
+
|
|
264
|
+
expect(cacheManager.getStats().totalSize).toBe(3);
|
|
265
|
+
|
|
266
|
+
const invalidated = WordPressCachePatterns.invalidateTaxonomies(httpCache);
|
|
267
|
+
|
|
268
|
+
expect(invalidated).toBeGreaterThan(0);
|
|
269
|
+
// Should invalidate categories and tags but not posts
|
|
270
|
+
expect(cacheManager.getStats().totalSize).toBeLessThan(3);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('should invalidate user-related caches', () => {
|
|
274
|
+
httpCache.warm('users', []);
|
|
275
|
+
httpCache.warm('users/me', {});
|
|
276
|
+
httpCache.warm('posts', []);
|
|
277
|
+
|
|
278
|
+
expect(cacheManager.getStats().totalSize).toBe(3);
|
|
279
|
+
|
|
280
|
+
const invalidated = WordPressCachePatterns.invalidateUsers(httpCache);
|
|
281
|
+
|
|
282
|
+
expect(invalidated).toBeGreaterThan(0);
|
|
283
|
+
// Should invalidate user caches but not posts
|
|
284
|
+
expect(cacheManager.getStats().totalSize).toBeLessThan(3);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('should invalidate all caches', () => {
|
|
288
|
+
httpCache.warm('posts', []);
|
|
289
|
+
httpCache.warm('categories', []);
|
|
290
|
+
httpCache.warm('users', []);
|
|
291
|
+
|
|
292
|
+
expect(cacheManager.getStats().totalSize).toBe(3);
|
|
293
|
+
|
|
294
|
+
const invalidated = WordPressCachePatterns.invalidateAll(httpCache);
|
|
295
|
+
|
|
296
|
+
expect(invalidated).toBe(3);
|
|
297
|
+
expect(cacheManager.getStats().totalSize).toBe(0);
|
|
298
|
+
});
|
|
299
|
+
});
|