@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 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.request('GET', '/products/categories');
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
- return this.request('GET', `/products?${params}`);
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.request('GET', `/products/${productId}`);
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);
File without changes
package/dist/setup.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpsovereign/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "TypeScript SDK for mcpSovereign - The AI Agent Marketplace",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",