@mcpsovereign/sdk 0.2.2 → 0.2.4
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/dist/index.d.ts +38 -0
- package/dist/index.js +99 -6
- package/dist/local-cache.d.ts +86 -0
- package/dist/local-cache.js +257 -0
- package/dist/mcp-server.js +0 -0
- package/dist/setup.js +0 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,10 +8,13 @@ export { OnboardingWizard } from './onboarding/wizard.js';
|
|
|
8
8
|
export { AgentHelperMCP, HELPER_TOOLS } from './mcp-helper/index.js';
|
|
9
9
|
export type { MCPTool } from './mcp-helper/index.js';
|
|
10
10
|
export { SOVEREIGN_STARTER_PACK, STARTER_CREDITS, PRODUCT_IDEAS, FEE_STRUCTURE, PLATFORM_CREDENTIALS, DEMO_PURCHASE_FLOW } from './onboarding/starter-kit.js';
|
|
11
|
+
export { LocalCache, createLocalCache, conditionalFetch, batchFetch, prefetchCommonData, CACHE_TTLS, } from './local-cache.js';
|
|
11
12
|
export interface SovereignConfig {
|
|
12
13
|
baseUrl?: string;
|
|
13
14
|
authToken?: string;
|
|
14
15
|
localStorePath?: string;
|
|
16
|
+
enableLocalCache?: boolean;
|
|
17
|
+
prefetchOnInit?: boolean;
|
|
15
18
|
}
|
|
16
19
|
export interface ApiResponse<T> {
|
|
17
20
|
success: boolean;
|
|
@@ -263,8 +266,33 @@ export declare class SovereignClient {
|
|
|
263
266
|
private baseUrl;
|
|
264
267
|
private authToken;
|
|
265
268
|
localStore: LocalStoreManager;
|
|
269
|
+
private localCache;
|
|
270
|
+
private enableLocalCache;
|
|
271
|
+
private prefetchOnInit;
|
|
272
|
+
private initialized;
|
|
266
273
|
constructor(config?: SovereignConfig);
|
|
274
|
+
/**
|
|
275
|
+
* Initialize SDK with prefetching
|
|
276
|
+
* Call this after setting auth token to prefetch common data
|
|
277
|
+
*/
|
|
278
|
+
initialize(): Promise<void>;
|
|
279
|
+
/**
|
|
280
|
+
* Get cache statistics (for debugging/monitoring)
|
|
281
|
+
*/
|
|
282
|
+
getCacheStats(): {
|
|
283
|
+
size: number;
|
|
284
|
+
maxSize: number;
|
|
285
|
+
entries: string[];
|
|
286
|
+
} | null;
|
|
287
|
+
/**
|
|
288
|
+
* Clear local cache (useful after logout or data changes)
|
|
289
|
+
*/
|
|
290
|
+
clearCache(): void;
|
|
267
291
|
private request;
|
|
292
|
+
/**
|
|
293
|
+
* Cached request - uses local cache with stale-while-revalidate
|
|
294
|
+
*/
|
|
295
|
+
private cachedRequest;
|
|
268
296
|
authenticate(walletAddress: string, signMessage?: (message: string) => Promise<string>): Promise<ApiResponse<{
|
|
269
297
|
token: string;
|
|
270
298
|
agent: Agent;
|
|
@@ -316,7 +344,14 @@ export declare class SovereignClient {
|
|
|
316
344
|
total_cost: string;
|
|
317
345
|
discount: number;
|
|
318
346
|
}>>;
|
|
347
|
+
/**
|
|
348
|
+
* Get product categories (cached locally for 1 hour)
|
|
349
|
+
*/
|
|
319
350
|
getCategories(): Promise<ApiResponse<ProductCategory[]>>;
|
|
351
|
+
/**
|
|
352
|
+
* Browse products (cached locally for 5 minutes)
|
|
353
|
+
* Uses stale-while-revalidate for smooth UX
|
|
354
|
+
*/
|
|
320
355
|
browseProducts(options?: {
|
|
321
356
|
category?: string;
|
|
322
357
|
search?: string;
|
|
@@ -329,6 +364,9 @@ export declare class SovereignClient {
|
|
|
329
364
|
page: number;
|
|
330
365
|
limit: number;
|
|
331
366
|
}>>;
|
|
367
|
+
/**
|
|
368
|
+
* Get product details (cached locally for 5 minutes)
|
|
369
|
+
*/
|
|
332
370
|
getProductDetails(productId: string): Promise<ApiResponse<Product & {
|
|
333
371
|
reviews: object[];
|
|
334
372
|
}>>;
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export { OnboardingWizard } from './onboarding/wizard.js';
|
|
|
16
16
|
export { AgentHelperMCP, HELPER_TOOLS } from './mcp-helper/index.js';
|
|
17
17
|
// Re-export starter kit
|
|
18
18
|
export { SOVEREIGN_STARTER_PACK, STARTER_CREDITS, PRODUCT_IDEAS, FEE_STRUCTURE, PLATFORM_CREDENTIALS, DEMO_PURCHASE_FLOW } from './onboarding/starter-kit.js';
|
|
19
|
+
// Re-export local cache (client-side load offloading)
|
|
20
|
+
export { LocalCache, createLocalCache, conditionalFetch, batchFetch, prefetchCommonData, CACHE_TTLS, } from './local-cache.js';
|
|
21
|
+
// Import for internal use
|
|
22
|
+
import { createLocalCache, prefetchCommonData, CACHE_TTLS } from './local-cache.js';
|
|
19
23
|
// =============================================================================
|
|
20
24
|
// Local Store Manager (runs locally, no credits needed)
|
|
21
25
|
// =============================================================================
|
|
@@ -225,33 +229,89 @@ export class SovereignClient {
|
|
|
225
229
|
baseUrl;
|
|
226
230
|
authToken;
|
|
227
231
|
localStore;
|
|
232
|
+
localCache = null;
|
|
233
|
+
enableLocalCache;
|
|
234
|
+
prefetchOnInit;
|
|
235
|
+
initialized = false;
|
|
228
236
|
constructor(config = {}) {
|
|
229
237
|
this.baseUrl = config.baseUrl || 'http://localhost:3100/api/v1';
|
|
230
238
|
this.authToken = config.authToken || null;
|
|
231
239
|
this.localStore = new LocalStoreManager(config.localStorePath);
|
|
240
|
+
this.enableLocalCache = config.enableLocalCache !== false; // Default true
|
|
241
|
+
this.prefetchOnInit = config.prefetchOnInit !== false; // Default true
|
|
242
|
+
// Initialize local cache for client-side load offloading
|
|
243
|
+
if (this.enableLocalCache) {
|
|
244
|
+
this.localCache = createLocalCache({
|
|
245
|
+
maxEntries: 500,
|
|
246
|
+
defaultTTL: CACHE_TTLS.listings,
|
|
247
|
+
staleWindow: 60 * 1000, // 1 minute stale-while-revalidate
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Initialize SDK with prefetching
|
|
253
|
+
* Call this after setting auth token to prefetch common data
|
|
254
|
+
*/
|
|
255
|
+
async initialize() {
|
|
256
|
+
if (this.initialized)
|
|
257
|
+
return;
|
|
258
|
+
// Load local store from disk
|
|
259
|
+
await this.localStore.load();
|
|
260
|
+
// Prefetch common data to reduce server calls
|
|
261
|
+
if (this.enableLocalCache && this.localCache && this.prefetchOnInit) {
|
|
262
|
+
await prefetchCommonData(this.localCache, this.baseUrl.replace('/api/v1', ''), this.authToken || undefined);
|
|
263
|
+
}
|
|
264
|
+
this.initialized = true;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get cache statistics (for debugging/monitoring)
|
|
268
|
+
*/
|
|
269
|
+
getCacheStats() {
|
|
270
|
+
return this.localCache?.getStats() || null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Clear local cache (useful after logout or data changes)
|
|
274
|
+
*/
|
|
275
|
+
clearCache() {
|
|
276
|
+
this.localCache?.clear();
|
|
232
277
|
}
|
|
233
278
|
// ---------------------------------------------------------------------------
|
|
234
279
|
// HTTP Methods
|
|
235
280
|
// ---------------------------------------------------------------------------
|
|
236
|
-
async request(method, path, body) {
|
|
281
|
+
async request(method, path, body, options) {
|
|
237
282
|
const headers = {
|
|
238
283
|
'Content-Type': 'application/json',
|
|
239
284
|
};
|
|
240
285
|
if (this.authToken) {
|
|
241
286
|
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
242
287
|
}
|
|
288
|
+
// Add ETag for conditional requests
|
|
289
|
+
if (options?.etag) {
|
|
290
|
+
headers['If-None-Match'] = options.etag;
|
|
291
|
+
}
|
|
243
292
|
try {
|
|
244
293
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
245
294
|
method,
|
|
246
295
|
headers,
|
|
247
296
|
body: body ? JSON.stringify(body) : undefined,
|
|
248
297
|
});
|
|
298
|
+
// Handle 304 Not Modified (ETag match)
|
|
299
|
+
if (response.status === 304) {
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
data: null,
|
|
303
|
+
notModified: true,
|
|
304
|
+
etag: options?.etag,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
249
307
|
const json = await response.json();
|
|
250
|
-
// Extract billing info from headers
|
|
308
|
+
// Extract billing info and ETag from headers
|
|
251
309
|
const creditsCharged = response.headers.get('X-Credits-Charged');
|
|
252
310
|
const creditsRemaining = response.headers.get('X-Credits-Remaining');
|
|
311
|
+
const newEtag = response.headers.get('ETag') || undefined;
|
|
253
312
|
return {
|
|
254
313
|
...json,
|
|
314
|
+
etag: newEtag,
|
|
255
315
|
headers: {
|
|
256
316
|
creditsCharged: creditsCharged ? parseInt(creditsCharged) : undefined,
|
|
257
317
|
creditsRemaining: creditsRemaining ? parseInt(creditsRemaining) : undefined
|
|
@@ -269,6 +329,27 @@ export class SovereignClient {
|
|
|
269
329
|
};
|
|
270
330
|
}
|
|
271
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Cached request - uses local cache with stale-while-revalidate
|
|
334
|
+
*/
|
|
335
|
+
async cachedRequest(cacheKey, path, ttl = CACHE_TTLS.listings) {
|
|
336
|
+
if (!this.localCache) {
|
|
337
|
+
return this.request('GET', path);
|
|
338
|
+
}
|
|
339
|
+
// Try cache first with stale-while-revalidate
|
|
340
|
+
const cachedData = await this.localCache.get(cacheKey, async () => {
|
|
341
|
+
const response = await this.request('GET', path);
|
|
342
|
+
if (response.success && response.data) {
|
|
343
|
+
return response.data;
|
|
344
|
+
}
|
|
345
|
+
throw new Error(response.error?.message || 'Request failed');
|
|
346
|
+
}, ttl);
|
|
347
|
+
if (cachedData) {
|
|
348
|
+
return { success: true, data: cachedData };
|
|
349
|
+
}
|
|
350
|
+
// Fallback to direct request
|
|
351
|
+
return this.request('GET', path);
|
|
352
|
+
}
|
|
272
353
|
// ---------------------------------------------------------------------------
|
|
273
354
|
// Authentication (FREE)
|
|
274
355
|
// ---------------------------------------------------------------------------
|
|
@@ -365,11 +446,18 @@ export class SovereignClient {
|
|
|
365
446
|
return this.request('POST', `/plots/${plotId}/rent`, { months });
|
|
366
447
|
}
|
|
367
448
|
// ---------------------------------------------------------------------------
|
|
368
|
-
// Products (Remote Marketplace)
|
|
449
|
+
// Products (Remote Marketplace) - Uses local cache for browse operations
|
|
369
450
|
// ---------------------------------------------------------------------------
|
|
451
|
+
/**
|
|
452
|
+
* Get product categories (cached locally for 1 hour)
|
|
453
|
+
*/
|
|
370
454
|
async getCategories() {
|
|
371
|
-
return this.
|
|
455
|
+
return this.cachedRequest('categories', '/products/categories', CACHE_TTLS.static);
|
|
372
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Browse products (cached locally for 5 minutes)
|
|
459
|
+
* Uses stale-while-revalidate for smooth UX
|
|
460
|
+
*/
|
|
373
461
|
async browseProducts(options = {}) {
|
|
374
462
|
const params = new URLSearchParams();
|
|
375
463
|
if (options.category)
|
|
@@ -382,10 +470,15 @@ export class SovereignClient {
|
|
|
382
470
|
params.set('limit', options.limit.toString());
|
|
383
471
|
if (options.sort)
|
|
384
472
|
params.set('sort', options.sort);
|
|
385
|
-
|
|
473
|
+
// Cache key includes all filter params
|
|
474
|
+
const cacheKey = `products:${options.category || 'all'}:${options.search || ''}:${options.page || 1}:${options.limit || 20}:${options.sort || 'newest'}`;
|
|
475
|
+
return this.cachedRequest(cacheKey, `/products?${params}`, CACHE_TTLS.listings);
|
|
386
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Get product details (cached locally for 5 minutes)
|
|
479
|
+
*/
|
|
387
480
|
async getProductDetails(productId) {
|
|
388
|
-
return this.
|
|
481
|
+
return this.cachedRequest(`product:${productId}`, `/products/${productId}`, CACHE_TTLS.listings);
|
|
389
482
|
}
|
|
390
483
|
async purchaseProduct(productId) {
|
|
391
484
|
return this.request('POST', `/products/${productId}/purchase`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface CacheConfig {
|
|
2
|
+
defaultTTL: number;
|
|
3
|
+
maxEntries: number;
|
|
4
|
+
staleWindow: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const CACHE_TTLS: {
|
|
7
|
+
readonly static: number;
|
|
8
|
+
readonly catalog: number;
|
|
9
|
+
readonly listings: number;
|
|
10
|
+
readonly realtime: number;
|
|
11
|
+
readonly user: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Local cache for SDK - reduces server calls by caching on the client
|
|
15
|
+
*/
|
|
16
|
+
export declare class LocalCache {
|
|
17
|
+
private cache;
|
|
18
|
+
private config;
|
|
19
|
+
private pendingRequests;
|
|
20
|
+
constructor(config?: Partial<CacheConfig>);
|
|
21
|
+
/**
|
|
22
|
+
* Get from cache, optionally fetching if stale/missing
|
|
23
|
+
*/
|
|
24
|
+
get<T>(key: string, fetcher?: () => Promise<T>, ttl?: number): Promise<T | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Fetch with request deduplication
|
|
27
|
+
*/
|
|
28
|
+
private fetchAndCache;
|
|
29
|
+
/**
|
|
30
|
+
* Set a value in cache
|
|
31
|
+
*/
|
|
32
|
+
set<T>(key: string, data: T, ttl?: number, etag?: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Check if cache has fresh data for key
|
|
35
|
+
*/
|
|
36
|
+
has(key: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get the ETag for conditional requests
|
|
39
|
+
*/
|
|
40
|
+
getETag(key: string): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Invalidate specific key
|
|
43
|
+
*/
|
|
44
|
+
invalidate(key: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Invalidate all keys matching prefix
|
|
47
|
+
*/
|
|
48
|
+
invalidatePrefix(prefix: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Clear entire cache
|
|
51
|
+
*/
|
|
52
|
+
clear(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Get cache stats
|
|
55
|
+
*/
|
|
56
|
+
getStats(): {
|
|
57
|
+
size: number;
|
|
58
|
+
maxSize: number;
|
|
59
|
+
entries: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Make a conditional request using ETag
|
|
64
|
+
* Server returns 304 Not Modified if data hasn't changed
|
|
65
|
+
*/
|
|
66
|
+
export declare function conditionalFetch<T>(url: string, etag: string | undefined, options?: RequestInit): Promise<{
|
|
67
|
+
data: T | null;
|
|
68
|
+
etag: string | undefined;
|
|
69
|
+
modified: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
interface BatchItem<T> {
|
|
72
|
+
key: string;
|
|
73
|
+
fetcher: () => Promise<T>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Batch multiple requests into one with local caching
|
|
77
|
+
* Reduces round trips to server
|
|
78
|
+
*/
|
|
79
|
+
export declare function batchFetch<T>(cache: LocalCache, items: BatchItem<T>[], ttl?: number): Promise<Map<string, T>>;
|
|
80
|
+
/**
|
|
81
|
+
* Prefetch common data on SDK initialization
|
|
82
|
+
* Reduces latency for common operations
|
|
83
|
+
*/
|
|
84
|
+
export declare function prefetchCommonData(cache: LocalCache, apiBase: string, token?: string): Promise<void>;
|
|
85
|
+
export declare const createLocalCache: (config?: Partial<CacheConfig>) => LocalCache;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SDK Local Cache - Client-Side Load Offloading
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Push browsing/catalog data to the client to reduce server load.
|
|
5
|
+
// The SDK maintains a local cache that syncs efficiently with the server.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
defaultTTL: 5 * 60 * 1000, // 5 minutes
|
|
9
|
+
maxEntries: 1000,
|
|
10
|
+
staleWindow: 60 * 1000, // 1 minute stale-while-revalidate
|
|
11
|
+
};
|
|
12
|
+
// TTLs by data type
|
|
13
|
+
export const CACHE_TTLS = {
|
|
14
|
+
// Static data - levels, achievements, tiers (cache for hours)
|
|
15
|
+
static: 60 * 60 * 1000, // 1 hour
|
|
16
|
+
// Semi-static - cosmetics, loot crates, categories
|
|
17
|
+
catalog: 30 * 60 * 1000, // 30 minutes
|
|
18
|
+
// Dynamic - product listings, clan lists
|
|
19
|
+
listings: 5 * 60 * 1000, // 5 minutes
|
|
20
|
+
// Frequently changing - leaderboards, dashboards
|
|
21
|
+
realtime: 60 * 1000, // 1 minute
|
|
22
|
+
// User-specific - balance, profile
|
|
23
|
+
user: 30 * 1000, // 30 seconds
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Local cache for SDK - reduces server calls by caching on the client
|
|
27
|
+
*/
|
|
28
|
+
export class LocalCache {
|
|
29
|
+
cache = new Map();
|
|
30
|
+
config;
|
|
31
|
+
pendingRequests = new Map();
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get from cache, optionally fetching if stale/missing
|
|
37
|
+
*/
|
|
38
|
+
async get(key, fetcher, ttl = this.config.defaultTTL) {
|
|
39
|
+
const entry = this.cache.get(key);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
// Fresh cache hit
|
|
42
|
+
if (entry && (now - entry.timestamp) < entry.ttl) {
|
|
43
|
+
return entry.data;
|
|
44
|
+
}
|
|
45
|
+
// Stale-while-revalidate: return stale data while fetching fresh
|
|
46
|
+
if (entry && (now - entry.timestamp) < (entry.ttl + this.config.staleWindow)) {
|
|
47
|
+
if (fetcher) {
|
|
48
|
+
// Background refresh (don't await)
|
|
49
|
+
this.fetchAndCache(key, fetcher, ttl).catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
return entry.data;
|
|
52
|
+
}
|
|
53
|
+
// Cache miss or expired - fetch if we have a fetcher
|
|
54
|
+
if (fetcher) {
|
|
55
|
+
return this.fetchAndCache(key, fetcher, ttl);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fetch with request deduplication
|
|
61
|
+
*/
|
|
62
|
+
async fetchAndCache(key, fetcher, ttl) {
|
|
63
|
+
// Check if request is already in flight (deduplication)
|
|
64
|
+
const pending = this.pendingRequests.get(key);
|
|
65
|
+
if (pending) {
|
|
66
|
+
return pending;
|
|
67
|
+
}
|
|
68
|
+
// Start the fetch
|
|
69
|
+
const fetchPromise = (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const data = await fetcher();
|
|
72
|
+
this.set(key, data, ttl);
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.pendingRequests.delete(key);
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
this.pendingRequests.set(key, fetchPromise);
|
|
80
|
+
return fetchPromise;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set a value in cache
|
|
84
|
+
*/
|
|
85
|
+
set(key, data, ttl = this.config.defaultTTL, etag) {
|
|
86
|
+
// Enforce max entries (LRU eviction)
|
|
87
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
88
|
+
const oldestKey = this.cache.keys().next().value;
|
|
89
|
+
if (oldestKey) {
|
|
90
|
+
this.cache.delete(oldestKey);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.cache.set(key, {
|
|
94
|
+
data,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
ttl,
|
|
97
|
+
etag,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if cache has fresh data for key
|
|
102
|
+
*/
|
|
103
|
+
has(key) {
|
|
104
|
+
const entry = this.cache.get(key);
|
|
105
|
+
if (!entry)
|
|
106
|
+
return false;
|
|
107
|
+
return (Date.now() - entry.timestamp) < entry.ttl;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the ETag for conditional requests
|
|
111
|
+
*/
|
|
112
|
+
getETag(key) {
|
|
113
|
+
return this.cache.get(key)?.etag;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Invalidate specific key
|
|
117
|
+
*/
|
|
118
|
+
invalidate(key) {
|
|
119
|
+
this.cache.delete(key);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Invalidate all keys matching prefix
|
|
123
|
+
*/
|
|
124
|
+
invalidatePrefix(prefix) {
|
|
125
|
+
for (const key of this.cache.keys()) {
|
|
126
|
+
if (key.startsWith(prefix)) {
|
|
127
|
+
this.cache.delete(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Clear entire cache
|
|
133
|
+
*/
|
|
134
|
+
clear() {
|
|
135
|
+
this.cache.clear();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get cache stats
|
|
139
|
+
*/
|
|
140
|
+
getStats() {
|
|
141
|
+
return {
|
|
142
|
+
size: this.cache.size,
|
|
143
|
+
maxSize: this.config.maxEntries,
|
|
144
|
+
entries: Array.from(this.cache.keys()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// Conditional Request Helpers
|
|
150
|
+
// =============================================================================
|
|
151
|
+
/**
|
|
152
|
+
* Make a conditional request using ETag
|
|
153
|
+
* Server returns 304 Not Modified if data hasn't changed
|
|
154
|
+
*/
|
|
155
|
+
export async function conditionalFetch(url, etag, options = {}) {
|
|
156
|
+
const headers = {
|
|
157
|
+
...(options.headers || {}),
|
|
158
|
+
};
|
|
159
|
+
if (etag) {
|
|
160
|
+
headers['If-None-Match'] = etag;
|
|
161
|
+
}
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
...options,
|
|
164
|
+
headers,
|
|
165
|
+
});
|
|
166
|
+
// Not modified - use cached data
|
|
167
|
+
if (response.status === 304) {
|
|
168
|
+
return {
|
|
169
|
+
data: null,
|
|
170
|
+
etag,
|
|
171
|
+
modified: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const data = await response.json();
|
|
175
|
+
const newEtag = response.headers.get('ETag') || undefined;
|
|
176
|
+
return {
|
|
177
|
+
data: data,
|
|
178
|
+
etag: newEtag,
|
|
179
|
+
modified: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Batch multiple requests into one with local caching
|
|
184
|
+
* Reduces round trips to server
|
|
185
|
+
*/
|
|
186
|
+
export async function batchFetch(cache, items, ttl = CACHE_TTLS.listings) {
|
|
187
|
+
const results = new Map();
|
|
188
|
+
const toFetch = [];
|
|
189
|
+
// Check cache first
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
const cached = await cache.get(item.key);
|
|
192
|
+
if (cached) {
|
|
193
|
+
results.set(item.key, cached);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
toFetch.push(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Fetch missing items in parallel
|
|
200
|
+
if (toFetch.length > 0) {
|
|
201
|
+
const fetchPromises = toFetch.map(async (item) => {
|
|
202
|
+
try {
|
|
203
|
+
const data = await item.fetcher();
|
|
204
|
+
cache.set(item.key, data, ttl);
|
|
205
|
+
results.set(item.key, data);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
// Individual failures don't break the batch
|
|
209
|
+
console.warn(`Failed to fetch ${item.key}:`, error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
await Promise.all(fetchPromises);
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// Prefetch Strategy
|
|
218
|
+
// =============================================================================
|
|
219
|
+
/**
|
|
220
|
+
* Prefetch common data on SDK initialization
|
|
221
|
+
* Reduces latency for common operations
|
|
222
|
+
*/
|
|
223
|
+
export async function prefetchCommonData(cache, apiBase, token) {
|
|
224
|
+
const headers = token
|
|
225
|
+
? { Authorization: `Bearer ${token}` }
|
|
226
|
+
: {};
|
|
227
|
+
const prefetchItems = [
|
|
228
|
+
// Static data - fetch once, cache long
|
|
229
|
+
{ key: 'levels', url: '/api/v1/gamification/levels', ttl: CACHE_TTLS.static },
|
|
230
|
+
{ key: 'achievements', url: '/api/v1/gamification/achievements', ttl: CACHE_TTLS.static },
|
|
231
|
+
{ key: 'trades', url: '/api/v1/trades', ttl: CACHE_TTLS.static },
|
|
232
|
+
{ key: 'clan-tiers', url: '/api/v1/clans/tiers', ttl: CACHE_TTLS.static },
|
|
233
|
+
{ key: 'clan-roles', url: '/api/v1/clans/roles', ttl: CACHE_TTLS.static },
|
|
234
|
+
// Catalog data - cache moderately
|
|
235
|
+
{ key: 'categories', url: '/api/v1/products/categories', ttl: CACHE_TTLS.catalog },
|
|
236
|
+
{ key: 'cosmetics', url: '/api/v1/monetization/cosmetics', ttl: CACHE_TTLS.catalog },
|
|
237
|
+
{ key: 'loot-crates', url: '/api/v1/monetization/loot', ttl: CACHE_TTLS.catalog },
|
|
238
|
+
{ key: 'subscriptions', url: '/api/v1/monetization/subscriptions', ttl: CACHE_TTLS.catalog },
|
|
239
|
+
];
|
|
240
|
+
// Fetch in parallel but don't fail if some fail
|
|
241
|
+
await Promise.allSettled(prefetchItems.map(async (item) => {
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(`${apiBase}${item.url}`, { headers });
|
|
244
|
+
if (response.ok) {
|
|
245
|
+
const json = await response.json();
|
|
246
|
+
cache.set(item.key, json.data ?? json, item.ttl);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Silent fail - prefetch is best-effort
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// Exports
|
|
256
|
+
// =============================================================================
|
|
257
|
+
export const createLocalCache = (config) => new LocalCache(config);
|
package/dist/mcp-server.js
CHANGED
|
File without changes
|
package/dist/setup.js
CHANGED
|
File without changes
|