@jgardner04/ghost-mcp-server 1.1.0 → 1.1.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 +0 -3
- package/package.json +20 -2
- package/src/config/mcp-config.js +31 -22
- package/src/controllers/imageController.js +62 -62
- package/src/controllers/postController.js +8 -8
- package/src/controllers/tagController.js +17 -20
- package/src/errors/index.js +49 -44
- package/src/index.js +56 -50
- package/src/mcp_server.js +151 -178
- package/src/mcp_server_enhanced.js +265 -259
- package/src/mcp_server_improved.js +104 -71
- package/src/middleware/errorMiddleware.js +69 -70
- package/src/resources/ResourceManager.js +143 -134
- package/src/routes/imageRoutes.js +9 -9
- package/src/routes/postRoutes.js +22 -28
- package/src/routes/tagRoutes.js +12 -14
- package/src/services/__tests__/ghostService.test.js +20 -18
- package/src/services/ghostService.js +34 -46
- package/src/services/ghostServiceImproved.js +125 -109
- package/src/services/imageProcessingService.js +15 -15
- package/src/services/postService.js +22 -22
- package/src/utils/logger.js +50 -50
- package/src/utils/urlValidator.js +37 -38
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
* Provides caching, pagination, filtering, and subscription capabilities
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Resource } from
|
|
7
|
-
import { NotFoundError, ValidationError
|
|
6
|
+
import { Resource } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { NotFoundError, ValidationError } from '../errors/index.js';
|
|
8
8
|
import EventEmitter from 'events';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* LRU Cache implementation for resource caching
|
|
12
12
|
*/
|
|
13
13
|
class LRUCache {
|
|
14
|
-
constructor(maxSize = 100, ttl = 300000) {
|
|
14
|
+
constructor(maxSize = 100, ttl = 300000) {
|
|
15
|
+
// 5 minutes default TTL
|
|
15
16
|
this.maxSize = maxSize;
|
|
16
17
|
this.ttl = ttl;
|
|
17
18
|
this.cache = new Map();
|
|
@@ -20,20 +21,20 @@ class LRUCache {
|
|
|
20
21
|
|
|
21
22
|
get(key) {
|
|
22
23
|
const item = this.cache.get(key);
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
if (!item) return null;
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
// Check if expired
|
|
27
28
|
if (Date.now() > item.expiry) {
|
|
28
29
|
this.cache.delete(key);
|
|
29
|
-
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
30
|
+
this.accessOrder = this.accessOrder.filter((k) => k !== key);
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
// Update access order
|
|
34
|
-
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
35
|
+
this.accessOrder = this.accessOrder.filter((k) => k !== key);
|
|
35
36
|
this.accessOrder.push(key);
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
return item.value;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -43,13 +44,13 @@ class LRUCache {
|
|
|
43
44
|
const oldestKey = this.accessOrder.shift();
|
|
44
45
|
this.cache.delete(oldestKey);
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
const ttl = customTTL || this.ttl;
|
|
48
49
|
this.cache.set(key, {
|
|
49
50
|
value,
|
|
50
|
-
expiry: Date.now() + ttl
|
|
51
|
+
expiry: Date.now() + ttl,
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
this.accessOrder.push(key);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -59,13 +60,13 @@ class LRUCache {
|
|
|
59
60
|
this.accessOrder = [];
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
// Invalidate entries matching pattern
|
|
64
65
|
const regex = new RegExp(pattern);
|
|
65
66
|
for (const [key] of this.cache) {
|
|
66
67
|
if (regex.test(key)) {
|
|
67
68
|
this.cache.delete(key);
|
|
68
|
-
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
69
|
+
this.accessOrder = this.accessOrder.filter((k) => k !== key);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -75,7 +76,7 @@ class LRUCache {
|
|
|
75
76
|
size: this.cache.size,
|
|
76
77
|
maxSize: this.maxSize,
|
|
77
78
|
ttl: this.ttl,
|
|
78
|
-
keys: Array.from(this.cache.keys())
|
|
79
|
+
keys: Array.from(this.cache.keys()),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
}
|
|
@@ -91,57 +92,57 @@ class ResourceURIParser {
|
|
|
91
92
|
// - ghost/post/uuid:550e8400-e29b-41d4-a716-446655440000
|
|
92
93
|
// - ghost/posts?status=published&limit=10&page=2
|
|
93
94
|
// - ghost/tag/technology
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
const url = new URL(uri, 'resource://');
|
|
96
97
|
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
97
|
-
|
|
98
|
+
|
|
98
99
|
if (pathParts.length < 2) {
|
|
99
100
|
throw new ValidationError('Invalid resource URI format');
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
|
|
102
103
|
const [namespace, resourceType, ...identifierParts] = pathParts;
|
|
103
104
|
const identifier = identifierParts.join('/');
|
|
104
|
-
|
|
105
|
+
|
|
105
106
|
// Parse query parameters
|
|
106
107
|
const query = {};
|
|
107
108
|
for (const [key, value] of url.searchParams) {
|
|
108
109
|
query[key] = value;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
// Parse identifier type
|
|
112
113
|
let identifierType = 'id';
|
|
113
114
|
let identifierValue = identifier;
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
if (identifier && identifier.includes(':')) {
|
|
116
117
|
const [type, ...valueParts] = identifier.split(':');
|
|
117
118
|
identifierType = type;
|
|
118
119
|
identifierValue = valueParts.join(':');
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
return {
|
|
122
123
|
namespace,
|
|
123
124
|
resourceType,
|
|
124
125
|
identifier: identifierValue,
|
|
125
126
|
identifierType,
|
|
126
127
|
query,
|
|
127
|
-
isCollection: !identifier || Object.keys(query).length > 0
|
|
128
|
+
isCollection: !identifier || Object.keys(query).length > 0,
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
static build(parts) {
|
|
132
133
|
const { namespace, resourceType, identifier, query = {} } = parts;
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
let uri = `${namespace}/${resourceType}`;
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
if (identifier) {
|
|
137
138
|
uri += `/${identifier}`;
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
+
|
|
140
141
|
const queryString = new URLSearchParams(query).toString();
|
|
141
142
|
if (queryString) {
|
|
142
143
|
uri += `?${queryString}`;
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
+
|
|
145
146
|
return uri;
|
|
146
147
|
}
|
|
147
148
|
}
|
|
@@ -157,90 +158,92 @@ class ResourceFetcher {
|
|
|
157
158
|
|
|
158
159
|
async fetchPost(parsedURI) {
|
|
159
160
|
const { identifier, identifierType, query, isCollection } = parsedURI;
|
|
160
|
-
|
|
161
|
+
|
|
161
162
|
if (isCollection) {
|
|
162
163
|
return await this.fetchPosts(query);
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
|
|
165
166
|
const cacheKey = `post:${identifierType}:${identifier}`;
|
|
166
|
-
|
|
167
|
+
|
|
167
168
|
// Check cache
|
|
168
169
|
const cached = this.cache.get(cacheKey);
|
|
169
170
|
if (cached) {
|
|
170
171
|
console.log(`Cache hit for ${cacheKey}`);
|
|
171
172
|
return cached;
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
// Fetch from Ghost
|
|
175
176
|
let post;
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
switch (identifierType) {
|
|
178
179
|
case 'id':
|
|
179
180
|
post = await this.ghostService.getPost(identifier, { include: 'tags,authors' });
|
|
180
181
|
break;
|
|
181
|
-
|
|
182
|
-
case 'slug':
|
|
183
|
-
const posts = await this.ghostService.getPosts({
|
|
182
|
+
|
|
183
|
+
case 'slug': {
|
|
184
|
+
const posts = await this.ghostService.getPosts({
|
|
184
185
|
filter: `slug:${identifier}`,
|
|
185
186
|
include: 'tags,authors',
|
|
186
|
-
limit: 1
|
|
187
|
+
limit: 1,
|
|
187
188
|
});
|
|
188
189
|
post = posts[0];
|
|
189
190
|
break;
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'uuid': {
|
|
192
194
|
const postsByUuid = await this.ghostService.getPosts({
|
|
193
195
|
filter: `uuid:${identifier}`,
|
|
194
196
|
include: 'tags,authors',
|
|
195
|
-
limit: 1
|
|
197
|
+
limit: 1,
|
|
196
198
|
});
|
|
197
199
|
post = postsByUuid[0];
|
|
198
200
|
break;
|
|
199
|
-
|
|
201
|
+
}
|
|
202
|
+
|
|
200
203
|
default:
|
|
201
204
|
throw new ValidationError(`Unknown identifier type: ${identifierType}`);
|
|
202
205
|
}
|
|
203
|
-
|
|
206
|
+
|
|
204
207
|
if (!post) {
|
|
205
208
|
throw new NotFoundError('Post', identifier);
|
|
206
209
|
}
|
|
207
|
-
|
|
210
|
+
|
|
208
211
|
// Cache the result
|
|
209
212
|
this.cache.set(cacheKey, post);
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
return post;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
async fetchPosts(query = {}) {
|
|
215
218
|
// Build cache key from query
|
|
216
219
|
const cacheKey = `posts:${JSON.stringify(query)}`;
|
|
217
|
-
|
|
220
|
+
|
|
218
221
|
// Check cache
|
|
219
222
|
const cached = this.cache.get(cacheKey);
|
|
220
223
|
if (cached) {
|
|
221
224
|
console.log(`Cache hit for posts query`);
|
|
222
225
|
return cached;
|
|
223
226
|
}
|
|
224
|
-
|
|
227
|
+
|
|
225
228
|
// Parse query parameters
|
|
226
229
|
const options = {
|
|
227
230
|
limit: parseInt(query.limit) || 15,
|
|
228
231
|
page: parseInt(query.page) || 1,
|
|
229
232
|
include: query.include || 'tags,authors',
|
|
230
233
|
filter: query.filter,
|
|
231
|
-
order: query.order || 'published_at desc'
|
|
234
|
+
order: query.order || 'published_at desc',
|
|
232
235
|
};
|
|
233
|
-
|
|
236
|
+
|
|
234
237
|
// Add status filter if provided
|
|
235
238
|
if (query.status) {
|
|
236
|
-
options.filter = options.filter
|
|
239
|
+
options.filter = options.filter
|
|
237
240
|
? `${options.filter}+status:${query.status}`
|
|
238
241
|
: `status:${query.status}`;
|
|
239
242
|
}
|
|
240
|
-
|
|
243
|
+
|
|
241
244
|
// Fetch from Ghost
|
|
242
245
|
const result = await this.ghostService.getPosts(options);
|
|
243
|
-
|
|
246
|
+
|
|
244
247
|
// Format response with pagination metadata
|
|
245
248
|
const response = {
|
|
246
249
|
data: result,
|
|
@@ -251,100 +254,103 @@ class ResourceFetcher {
|
|
|
251
254
|
pages: Math.ceil(result.meta?.pagination?.total / options.limit) || 1,
|
|
252
255
|
total: result.meta?.pagination?.total || result.length,
|
|
253
256
|
next: result.meta?.pagination?.next || null,
|
|
254
|
-
prev: result.meta?.pagination?.prev || null
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
+
prev: result.meta?.pagination?.prev || null,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
257
260
|
};
|
|
258
|
-
|
|
261
|
+
|
|
259
262
|
// Cache with shorter TTL for collections
|
|
260
263
|
this.cache.set(cacheKey, response, 60000); // 1 minute for collections
|
|
261
|
-
|
|
264
|
+
|
|
262
265
|
return response;
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
async fetchTag(parsedURI) {
|
|
266
269
|
const { identifier, identifierType, query, isCollection } = parsedURI;
|
|
267
|
-
|
|
270
|
+
|
|
268
271
|
if (isCollection) {
|
|
269
272
|
return await this.fetchTags(query);
|
|
270
273
|
}
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
const cacheKey = `tag:${identifierType}:${identifier}`;
|
|
273
|
-
|
|
276
|
+
|
|
274
277
|
// Check cache
|
|
275
278
|
const cached = this.cache.get(cacheKey);
|
|
276
279
|
if (cached) {
|
|
277
280
|
console.log(`Cache hit for ${cacheKey}`);
|
|
278
281
|
return cached;
|
|
279
282
|
}
|
|
280
|
-
|
|
283
|
+
|
|
281
284
|
// Fetch from Ghost
|
|
282
285
|
let tag;
|
|
283
|
-
|
|
286
|
+
|
|
284
287
|
switch (identifierType) {
|
|
285
288
|
case 'id':
|
|
286
289
|
tag = await this.ghostService.getTag(identifier);
|
|
287
290
|
break;
|
|
288
|
-
|
|
289
|
-
case 'slug':
|
|
291
|
+
|
|
292
|
+
case 'slug': {
|
|
290
293
|
const tags = await this.ghostService.getTags();
|
|
291
|
-
tag = tags.find(t => t.slug === identifier);
|
|
294
|
+
tag = tags.find((t) => t.slug === identifier);
|
|
292
295
|
break;
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
case 'name': {
|
|
295
299
|
const tagsByName = await this.ghostService.getTags(identifier);
|
|
296
300
|
tag = tagsByName[0];
|
|
297
301
|
break;
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
default: {
|
|
300
305
|
// Assume it's a slug if no type specified
|
|
301
306
|
const tagsBySlug = await this.ghostService.getTags();
|
|
302
|
-
tag = tagsBySlug.find(t => t.slug === identifier || t.id === identifier);
|
|
307
|
+
tag = tagsBySlug.find((t) => t.slug === identifier || t.id === identifier);
|
|
308
|
+
}
|
|
303
309
|
}
|
|
304
|
-
|
|
310
|
+
|
|
305
311
|
if (!tag) {
|
|
306
312
|
throw new NotFoundError('Tag', identifier);
|
|
307
313
|
}
|
|
308
|
-
|
|
314
|
+
|
|
309
315
|
// Cache the result
|
|
310
316
|
this.cache.set(cacheKey, tag);
|
|
311
|
-
|
|
317
|
+
|
|
312
318
|
return tag;
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
async fetchTags(query = {}) {
|
|
316
322
|
const cacheKey = `tags:${JSON.stringify(query)}`;
|
|
317
|
-
|
|
323
|
+
|
|
318
324
|
// Check cache
|
|
319
325
|
const cached = this.cache.get(cacheKey);
|
|
320
326
|
if (cached) {
|
|
321
327
|
console.log(`Cache hit for tags query`);
|
|
322
328
|
return cached;
|
|
323
329
|
}
|
|
324
|
-
|
|
330
|
+
|
|
325
331
|
// Fetch from Ghost
|
|
326
332
|
const tags = await this.ghostService.getTags(query.name);
|
|
327
|
-
|
|
333
|
+
|
|
328
334
|
// Apply client-side filtering if needed
|
|
329
335
|
let filteredTags = tags;
|
|
330
|
-
|
|
336
|
+
|
|
331
337
|
if (query.filter) {
|
|
332
338
|
// Simple filtering implementation
|
|
333
339
|
const filters = query.filter.split('+');
|
|
334
|
-
filteredTags = tags.filter(tag => {
|
|
335
|
-
return filters.every(filter => {
|
|
340
|
+
filteredTags = tags.filter((tag) => {
|
|
341
|
+
return filters.every((filter) => {
|
|
336
342
|
const [field, value] = filter.split(':');
|
|
337
343
|
return tag[field]?.toString().toLowerCase().includes(value.toLowerCase());
|
|
338
344
|
});
|
|
339
345
|
});
|
|
340
346
|
}
|
|
341
|
-
|
|
347
|
+
|
|
342
348
|
// Apply pagination
|
|
343
349
|
const limit = parseInt(query.limit) || 50;
|
|
344
350
|
const page = parseInt(query.page) || 1;
|
|
345
351
|
const start = (page - 1) * limit;
|
|
346
352
|
const paginatedTags = filteredTags.slice(start, start + limit);
|
|
347
|
-
|
|
353
|
+
|
|
348
354
|
const response = {
|
|
349
355
|
data: paginatedTags,
|
|
350
356
|
meta: {
|
|
@@ -352,14 +358,14 @@ class ResourceFetcher {
|
|
|
352
358
|
page,
|
|
353
359
|
limit,
|
|
354
360
|
pages: Math.ceil(filteredTags.length / limit),
|
|
355
|
-
total: filteredTags.length
|
|
356
|
-
}
|
|
357
|
-
}
|
|
361
|
+
total: filteredTags.length,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
358
364
|
};
|
|
359
|
-
|
|
365
|
+
|
|
360
366
|
// Cache with shorter TTL for collections
|
|
361
367
|
this.cache.set(cacheKey, response, 60000);
|
|
362
|
-
|
|
368
|
+
|
|
363
369
|
return response;
|
|
364
370
|
}
|
|
365
371
|
}
|
|
@@ -378,52 +384,52 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
378
384
|
subscribe(uri, callback, options = {}) {
|
|
379
385
|
const subscriptionId = `sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
380
386
|
const pollingInterval = options.pollingInterval || 30000; // 30 seconds default
|
|
381
|
-
|
|
387
|
+
|
|
382
388
|
const subscription = {
|
|
383
389
|
id: subscriptionId,
|
|
384
390
|
uri,
|
|
385
391
|
callback,
|
|
386
392
|
lastValue: null,
|
|
387
|
-
options
|
|
393
|
+
options,
|
|
388
394
|
};
|
|
389
|
-
|
|
395
|
+
|
|
390
396
|
this.subscriptions.set(subscriptionId, subscription);
|
|
391
|
-
|
|
397
|
+
|
|
392
398
|
// Start polling if requested
|
|
393
399
|
if (options.enablePolling) {
|
|
394
400
|
this.startPolling(subscriptionId, pollingInterval);
|
|
395
401
|
}
|
|
396
|
-
|
|
402
|
+
|
|
397
403
|
console.log(`Created subscription ${subscriptionId} for ${uri}`);
|
|
398
|
-
|
|
404
|
+
|
|
399
405
|
return subscriptionId;
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
unsubscribe(subscriptionId) {
|
|
403
409
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
404
|
-
|
|
410
|
+
|
|
405
411
|
if (!subscription) {
|
|
406
412
|
throw new NotFoundError('Subscription', subscriptionId);
|
|
407
413
|
}
|
|
408
|
-
|
|
414
|
+
|
|
409
415
|
// Stop polling if active
|
|
410
416
|
this.stopPolling(subscriptionId);
|
|
411
|
-
|
|
417
|
+
|
|
412
418
|
// Remove subscription
|
|
413
419
|
this.subscriptions.delete(subscriptionId);
|
|
414
|
-
|
|
420
|
+
|
|
415
421
|
console.log(`Removed subscription ${subscriptionId}`);
|
|
416
422
|
}
|
|
417
423
|
|
|
418
424
|
startPolling(subscriptionId, interval) {
|
|
419
425
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
420
|
-
|
|
426
|
+
|
|
421
427
|
if (!subscription) return;
|
|
422
|
-
|
|
428
|
+
|
|
423
429
|
const pollFunc = async () => {
|
|
424
430
|
try {
|
|
425
431
|
const currentValue = await this.fetchResource(subscription.uri);
|
|
426
|
-
|
|
432
|
+
|
|
427
433
|
// Check if value changed
|
|
428
434
|
if (JSON.stringify(currentValue) !== JSON.stringify(subscription.lastValue)) {
|
|
429
435
|
subscription.lastValue = currentValue;
|
|
@@ -431,7 +437,7 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
431
437
|
type: 'update',
|
|
432
438
|
uri: subscription.uri,
|
|
433
439
|
data: currentValue,
|
|
434
|
-
timestamp: new Date().toISOString()
|
|
440
|
+
timestamp: new Date().toISOString(),
|
|
435
441
|
});
|
|
436
442
|
}
|
|
437
443
|
} catch (error) {
|
|
@@ -439,14 +445,14 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
439
445
|
type: 'error',
|
|
440
446
|
uri: subscription.uri,
|
|
441
447
|
error: error.message,
|
|
442
|
-
timestamp: new Date().toISOString()
|
|
448
|
+
timestamp: new Date().toISOString(),
|
|
443
449
|
});
|
|
444
450
|
}
|
|
445
451
|
};
|
|
446
|
-
|
|
452
|
+
|
|
447
453
|
// Initial fetch
|
|
448
454
|
pollFunc();
|
|
449
|
-
|
|
455
|
+
|
|
450
456
|
// Set up interval
|
|
451
457
|
const intervalId = setInterval(pollFunc, interval);
|
|
452
458
|
this.pollingIntervals.set(subscriptionId, intervalId);
|
|
@@ -454,7 +460,7 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
454
460
|
|
|
455
461
|
stopPolling(subscriptionId) {
|
|
456
462
|
const intervalId = this.pollingIntervals.get(subscriptionId);
|
|
457
|
-
|
|
463
|
+
|
|
458
464
|
if (intervalId) {
|
|
459
465
|
clearInterval(intervalId);
|
|
460
466
|
this.pollingIntervals.delete(subscriptionId);
|
|
@@ -466,7 +472,7 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
466
472
|
if (this.resourceFetcher && typeof this.resourceFetcher === 'function') {
|
|
467
473
|
return await this.resourceFetcher(uri);
|
|
468
474
|
}
|
|
469
|
-
|
|
475
|
+
|
|
470
476
|
// Fallback error if no fetcher is configured
|
|
471
477
|
throw new Error('Resource fetcher not configured for subscription manager');
|
|
472
478
|
}
|
|
@@ -478,7 +484,7 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
478
484
|
type: eventType,
|
|
479
485
|
uri,
|
|
480
486
|
data,
|
|
481
|
-
timestamp: new Date().toISOString()
|
|
487
|
+
timestamp: new Date().toISOString(),
|
|
482
488
|
});
|
|
483
489
|
}
|
|
484
490
|
}
|
|
@@ -486,9 +492,11 @@ class ResourceSubscriptionManager extends EventEmitter {
|
|
|
486
492
|
|
|
487
493
|
matchesSubscription(subscriptionURI, eventURI) {
|
|
488
494
|
// Simple matching - could be enhanced with wildcards
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
495
|
+
return (
|
|
496
|
+
subscriptionURI === eventURI ||
|
|
497
|
+
subscriptionURI.startsWith(eventURI) ||
|
|
498
|
+
eventURI.startsWith(subscriptionURI)
|
|
499
|
+
);
|
|
492
500
|
}
|
|
493
501
|
}
|
|
494
502
|
|
|
@@ -501,9 +509,7 @@ export class ResourceManager {
|
|
|
501
509
|
this.cache = new LRUCache(100, 300000); // 100 items, 5 min TTL
|
|
502
510
|
this.fetcher = new ResourceFetcher(ghostService, this.cache);
|
|
503
511
|
// Pass a bound fetchResource method to the subscription manager
|
|
504
|
-
this.subscriptionManager = new ResourceSubscriptionManager(
|
|
505
|
-
(uri) => this.fetchResource(uri)
|
|
506
|
-
);
|
|
512
|
+
this.subscriptionManager = new ResourceSubscriptionManager((uri) => this.fetchResource(uri));
|
|
507
513
|
this.resources = new Map();
|
|
508
514
|
}
|
|
509
515
|
|
|
@@ -515,14 +521,14 @@ export class ResourceManager {
|
|
|
515
521
|
name,
|
|
516
522
|
description: options.description,
|
|
517
523
|
schema,
|
|
518
|
-
fetch: async (uri) => this.fetchResource(uri)
|
|
524
|
+
fetch: async (uri) => this.fetchResource(uri),
|
|
519
525
|
});
|
|
520
|
-
|
|
526
|
+
|
|
521
527
|
this.resources.set(name, {
|
|
522
528
|
resource,
|
|
523
|
-
options
|
|
529
|
+
options,
|
|
524
530
|
});
|
|
525
|
-
|
|
531
|
+
|
|
526
532
|
return resource;
|
|
527
533
|
}
|
|
528
534
|
|
|
@@ -532,24 +538,27 @@ export class ResourceManager {
|
|
|
532
538
|
async fetchResource(uri) {
|
|
533
539
|
try {
|
|
534
540
|
const parsed = ResourceURIParser.parse(uri);
|
|
535
|
-
|
|
541
|
+
|
|
536
542
|
console.log('Fetching resource:', { uri: uri.substring(0, 100), parsed });
|
|
537
|
-
|
|
543
|
+
|
|
538
544
|
// Route to appropriate fetcher
|
|
539
545
|
switch (parsed.resourceType) {
|
|
540
546
|
case 'post':
|
|
541
547
|
case 'posts':
|
|
542
548
|
return await this.fetcher.fetchPost(parsed);
|
|
543
|
-
|
|
549
|
+
|
|
544
550
|
case 'tag':
|
|
545
551
|
case 'tags':
|
|
546
552
|
return await this.fetcher.fetchTag(parsed);
|
|
547
|
-
|
|
553
|
+
|
|
548
554
|
default:
|
|
549
555
|
throw new ValidationError(`Unknown resource type: ${parsed.resourceType}`);
|
|
550
556
|
}
|
|
551
557
|
} catch (error) {
|
|
552
|
-
console.error('Error fetching resource:', {
|
|
558
|
+
console.error('Error fetching resource:', {
|
|
559
|
+
uri: uri.substring(0, 100),
|
|
560
|
+
error: error.message,
|
|
561
|
+
});
|
|
553
562
|
throw error;
|
|
554
563
|
}
|
|
555
564
|
}
|
|
@@ -559,21 +568,21 @@ export class ResourceManager {
|
|
|
559
568
|
*/
|
|
560
569
|
listResources(filter = {}) {
|
|
561
570
|
const resources = [];
|
|
562
|
-
|
|
571
|
+
|
|
563
572
|
for (const [name, { resource, options }] of this.resources) {
|
|
564
573
|
// Apply filter if provided
|
|
565
574
|
if (filter.namespace && !name.startsWith(filter.namespace)) {
|
|
566
575
|
continue;
|
|
567
576
|
}
|
|
568
|
-
|
|
577
|
+
|
|
569
578
|
resources.push({
|
|
570
579
|
uri: name,
|
|
571
580
|
name: resource.name,
|
|
572
581
|
description: resource.description,
|
|
573
|
-
...options
|
|
582
|
+
...options,
|
|
574
583
|
});
|
|
575
584
|
}
|
|
576
|
-
|
|
585
|
+
|
|
577
586
|
return resources;
|
|
578
587
|
}
|
|
579
588
|
|
|
@@ -605,7 +614,7 @@ export class ResourceManager {
|
|
|
605
614
|
notifyChange(uri, data, eventType = 'update') {
|
|
606
615
|
// Invalidate cache for this resource
|
|
607
616
|
this.cache.invalidate(uri);
|
|
608
|
-
|
|
617
|
+
|
|
609
618
|
// Notify subscribers
|
|
610
619
|
this.subscriptionManager.notifySubscribers(uri, data, eventType);
|
|
611
620
|
}
|
|
@@ -623,7 +632,7 @@ export class ResourceManager {
|
|
|
623
632
|
async batchFetch(uris) {
|
|
624
633
|
const results = {};
|
|
625
634
|
const errors = {};
|
|
626
|
-
|
|
635
|
+
|
|
627
636
|
await Promise.all(
|
|
628
637
|
uris.map(async (uri) => {
|
|
629
638
|
try {
|
|
@@ -631,12 +640,12 @@ export class ResourceManager {
|
|
|
631
640
|
} catch (error) {
|
|
632
641
|
errors[uri] = {
|
|
633
642
|
message: error.message,
|
|
634
|
-
code: error.code
|
|
643
|
+
code: error.code,
|
|
635
644
|
};
|
|
636
645
|
}
|
|
637
646
|
})
|
|
638
647
|
);
|
|
639
|
-
|
|
648
|
+
|
|
640
649
|
return { results, errors };
|
|
641
650
|
}
|
|
642
651
|
|
|
@@ -645,22 +654,22 @@ export class ResourceManager {
|
|
|
645
654
|
*/
|
|
646
655
|
async prefetch(patterns) {
|
|
647
656
|
const prefetched = [];
|
|
648
|
-
|
|
657
|
+
|
|
649
658
|
for (const pattern of patterns) {
|
|
650
659
|
try {
|
|
651
|
-
|
|
660
|
+
await this.fetchResource(pattern);
|
|
652
661
|
prefetched.push({ pattern, status: 'success' });
|
|
653
662
|
} catch (error) {
|
|
654
|
-
prefetched.push({
|
|
655
|
-
pattern,
|
|
663
|
+
prefetched.push({
|
|
664
|
+
pattern,
|
|
656
665
|
status: 'error',
|
|
657
|
-
error: error.message
|
|
666
|
+
error: error.message,
|
|
658
667
|
});
|
|
659
668
|
}
|
|
660
669
|
}
|
|
661
|
-
|
|
670
|
+
|
|
662
671
|
return prefetched;
|
|
663
672
|
}
|
|
664
673
|
}
|
|
665
674
|
|
|
666
|
-
export default ResourceManager;
|
|
675
|
+
export default ResourceManager;
|