tessl install github:jeremylongshore/claude-code-plugins-plus-skills --skill evernote-performance-tuningOptimize Evernote integration performance. Use when improving response times, reducing API calls, or scaling Evernote integrations. Trigger with phrases like "evernote performance", "optimize evernote", "evernote speed", "evernote caching".
Review Score
77%
Validation Score
11/16
Implementation Score
65%
Activation Score
90%
Optimize Evernote API integration performance through caching, efficient API usage, connection pooling, and smart data retrieval strategies.
// services/cache-service.js
const Redis = require('ioredis');
class EvernoteCacheService {
constructor(redisUrl) {
this.redis = new Redis(redisUrl);
this.defaultTTL = 300; // 5 minutes
}
/**
* Cache key generators
*/
keys = {
notebooks: (userId) => `evernote:${userId}:notebooks`,
tags: (userId) => `evernote:${userId}:tags`,
note: (guid) => `evernote:note:${guid}`,
noteMetadata: (guid) => `evernote:note:${guid}:meta`,
search: (userId, query) => `evernote:${userId}:search:${this.hashQuery(query)}`,
syncState: (userId) => `evernote:${userId}:syncState`
};
hashQuery(query) {
const crypto = require('crypto');
return crypto.createHash('md5').update(query).digest('hex');
}
/**
* Cache notebooks (longer TTL - rarely change)
*/
async cacheNotebooks(userId, notebooks) {
const key = this.keys.notebooks(userId);
await this.redis.setex(key, 3600, JSON.stringify(notebooks)); // 1 hour
}
async getNotebooks(userId) {
const key = this.keys.notebooks(userId);
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
/**
* Cache note metadata (shorter TTL)
*/
async cacheNoteMetadata(guid, metadata) {
const key = this.keys.noteMetadata(guid);
await this.redis.setex(key, this.defaultTTL, JSON.stringify(metadata));
}
async getNoteMetadata(guid) {
const key = this.keys.noteMetadata(guid);
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
/**
* Cache full note content (configurable TTL)
*/
async cacheNote(guid, note, ttl = 300) {
const key = this.keys.note(guid);
await this.redis.setex(key, ttl, JSON.stringify(note));
}
async getNote(guid) {
const key = this.keys.note(guid);
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
/**
* Cache search results (short TTL)
*/
async cacheSearch(userId, query, results) {
const key = this.keys.search(userId, query);
await this.redis.setex(key, 60, JSON.stringify(results)); // 1 minute
}
async getSearch(userId, query) {
const key = this.keys.search(userId, query);
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
/**
* Invalidate cache on changes
*/
async invalidateNote(guid) {
await this.redis.del(this.keys.note(guid));
await this.redis.del(this.keys.noteMetadata(guid));
}
async invalidateUserCache(userId) {
const pattern = `evernote:${userId}:*`;
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
module.exports = EvernoteCacheService;// services/cached-evernote-client.js
const Evernote = require('evernote');
const EvernoteCacheService = require('./cache-service');
class CachedEvernoteClient {
constructor(accessToken, userId, cacheService) {
this.client = new Evernote.Client({
token: accessToken,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
this.noteStore = this.client.getNoteStore();
this.userId = userId;
this.cache = cacheService;
}
/**
* List notebooks with caching
*/
async listNotebooks(forceRefresh = false) {
if (!forceRefresh) {
const cached = await this.cache.getNotebooks(this.userId);
if (cached) {
console.log('Cache HIT: notebooks');
return cached;
}
}
console.log('Cache MISS: notebooks');
const notebooks = await this.noteStore.listNotebooks();
await this.cache.cacheNotebooks(this.userId, notebooks);
return notebooks;
}
/**
* Get note with caching
*/
async getNote(guid, options = {}) {
const {
withContent = true,
withResources = false,
forceRefresh = false
} = options;
// Only cache if requesting content without resources
const canCache = withContent && !withResources;
if (canCache && !forceRefresh) {
const cached = await this.cache.getNote(guid);
if (cached) {
console.log('Cache HIT: note', guid);
return cached;
}
}
console.log('Cache MISS: note', guid);
const note = await this.noteStore.getNote(
guid,
withContent,
withResources,
false,
false
);
if (canCache) {
await this.cache.cacheNote(guid, note);
}
return note;
}
/**
* Search with caching
*/
async search(query, options = {}) {
const { maxResults = 50, forceRefresh = false } = options;
if (!forceRefresh) {
const cached = await this.cache.getSearch(this.userId, query);
if (cached) {
console.log('Cache HIT: search', query);
return cached;
}
}
console.log('Cache MISS: search', query);
const filter = new Evernote.NoteStore.NoteFilter({ words: query });
const spec = new Evernote.NoteStore.NotesMetadataResultSpec({
includeTitle: true,
includeCreated: true,
includeUpdated: true,
includeTagGuids: true,
includeNotebookGuid: true
});
const results = await this.noteStore.findNotesMetadata(
filter,
0,
maxResults,
spec
);
await this.cache.cacheSearch(this.userId, query, results);
return results;
}
}
module.exports = CachedEvernoteClient;// utils/request-batcher.js
class RequestBatcher {
constructor(options = {}) {
this.batchSize = options.batchSize || 10;
this.batchDelay = options.batchDelay || 100;
this.queue = [];
this.processing = false;
}
/**
* Add request to batch
*/
async add(operation) {
return new Promise((resolve, reject) => {
this.queue.push({ operation, resolve, reject });
if (!this.processing) {
this.processBatch();
}
});
}
/**
* Process queued requests
*/
async processBatch() {
if (this.queue.length === 0) {
this.processing = false;
return;
}
this.processing = true;
// Take batch from queue
const batch = this.queue.splice(0, this.batchSize);
// Execute in parallel
await Promise.all(
batch.map(async ({ operation, resolve, reject }) => {
try {
const result = await operation();
resolve(result);
} catch (error) {
reject(error);
}
})
);
// Small delay before next batch
if (this.queue.length > 0) {
await new Promise(r => setTimeout(r, this.batchDelay));
this.processBatch();
} else {
this.processing = false;
}
}
}
module.exports = RequestBatcher;// services/optimized-note-service.js
class OptimizedNoteService {
constructor(noteStore, cache) {
this.noteStore = noteStore;
this.cache = cache;
}
/**
* Get minimal data needed (don't over-fetch)
*/
async getNotePreview(guid) {
// Only get metadata, not full content
return this.noteStore.getNote(guid, false, false, false, false);
}
/**
* Get notes with content in single request
*/
async getNotesWithContent(guids) {
// Evernote doesn't support batch note retrieval
// Optimize by parallelizing with rate limit awareness
const results = [];
const batchSize = 5;
for (let i = 0; i < guids.length; i += batchSize) {
const batch = guids.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(guid =>
this.noteStore.getNote(guid, true, false, false, false)
)
);
results.push(...batchResults);
// Small delay between batches
if (i + batchSize < guids.length) {
await new Promise(r => setTimeout(r, 100));
}
}
return results;
}
/**
* Get resources separately (more efficient for large files)
*/
async getNoteWithLazyResources(guid) {
// Get note without resource data
const note = await this.noteStore.getNote(guid, true, false, false, false);
// Create lazy loader for resources
note.loadResource = async (resourceGuid) => {
return this.noteStore.getResource(
resourceGuid,
true, // withData
false, // withRecognition
false, // withAttributes
false // withAlternateData
);
};
return note;
}
/**
* Efficient search with pagination
*/
async* searchPaginated(query, pageSize = 50) {
const filter = new Evernote.NoteStore.NoteFilter({ words: query });
const spec = new Evernote.NoteStore.NotesMetadataResultSpec({
includeTitle: true,
includeUpdated: true
});
let offset = 0;
let total = null;
while (total === null || offset < total) {
const result = await this.noteStore.findNotesMetadata(
filter,
offset,
pageSize,
spec
);
total = result.totalNotes;
for (const note of result.notes) {
yield note;
}
offset += result.notes.length;
}
}
}
module.exports = OptimizedNoteService;// utils/connection-manager.js
class ConnectionManager {
constructor() {
this.clients = new Map();
this.maxIdleTime = 5 * 60 * 1000; // 5 minutes
}
/**
* Get or create client for user
*/
getClient(userId, accessToken) {
const existing = this.clients.get(userId);
if (existing) {
existing.lastUsed = Date.now();
return existing.client;
}
const client = new Evernote.Client({
token: accessToken,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
this.clients.set(userId, {
client,
lastUsed: Date.now()
});
return client;
}
/**
* Clean up idle connections
*/
cleanup() {
const now = Date.now();
for (const [userId, data] of this.clients) {
if (now - data.lastUsed > this.maxIdleTime) {
this.clients.delete(userId);
console.log(`Cleaned up idle connection for user ${userId}`);
}
}
}
/**
* Start cleanup interval
*/
startCleanup(interval = 60000) {
setInterval(() => this.cleanup(), interval);
}
}
module.exports = ConnectionManager;// utils/performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = {
apiCalls: 0,
cacheHits: 0,
cacheMisses: 0,
totalLatency: 0,
errors: 0
};
this.callDurations = [];
}
/**
* Track API call
*/
trackCall(operation, duration, fromCache = false) {
this.metrics.apiCalls++;
this.metrics.totalLatency += duration;
this.callDurations.push({ operation, duration, fromCache, timestamp: Date.now() });
if (fromCache) {
this.metrics.cacheHits++;
} else {
this.metrics.cacheMisses++;
}
// Keep last 1000 calls
if (this.callDurations.length > 1000) {
this.callDurations.shift();
}
}
/**
* Track error
*/
trackError(operation, error) {
this.metrics.errors++;
console.error(`[PERF] Error in ${operation}:`, error.message);
}
/**
* Get performance stats
*/
getStats() {
const avgLatency = this.metrics.apiCalls > 0
? this.metrics.totalLatency / this.metrics.apiCalls
: 0;
const cacheHitRate = this.metrics.apiCalls > 0
? (this.metrics.cacheHits / this.metrics.apiCalls) * 100
: 0;
// Calculate p95 latency
const sortedDurations = [...this.callDurations]
.map(c => c.duration)
.sort((a, b) => a - b);
const p95Index = Math.floor(sortedDurations.length * 0.95);
const p95Latency = sortedDurations[p95Index] || 0;
return {
totalCalls: this.metrics.apiCalls,
cacheHits: this.metrics.cacheHits,
cacheMisses: this.metrics.cacheMisses,
cacheHitRate: `${cacheHitRate.toFixed(1)}%`,
avgLatencyMs: avgLatency.toFixed(2),
p95LatencyMs: p95Latency.toFixed(2),
errors: this.metrics.errors
};
}
/**
* Create instrumented wrapper
*/
instrument(noteStore) {
const monitor = this;
return new Proxy(noteStore, {
get(target, prop) {
const original = target[prop];
if (typeof original !== 'function') {
return original;
}
return async (...args) => {
const start = Date.now();
try {
const result = await original.apply(target, args);
const duration = Date.now() - start;
monitor.trackCall(prop, duration);
return result;
} catch (error) {
const duration = Date.now() - start;
monitor.trackCall(prop, duration);
monitor.trackError(prop, error);
throw error;
}
};
}
});
}
}
module.exports = PerformanceMonitor;// example-optimized.js
const Redis = require('ioredis');
const EvernoteCacheService = require('./services/cache-service');
const CachedEvernoteClient = require('./services/cached-evernote-client');
const PerformanceMonitor = require('./utils/performance-monitor');
async function main() {
const redis = new Redis(process.env.REDIS_URL);
const cache = new EvernoteCacheService(redis);
const monitor = new PerformanceMonitor();
const userId = 'user-123';
const client = new CachedEvernoteClient(
process.env.EVERNOTE_ACCESS_TOKEN,
userId,
cache
);
// Instrument for monitoring
client.noteStore = monitor.instrument(client.noteStore);
// First call - cache miss
console.log('\nFirst request (cache miss):');
console.time('First notebooks call');
await client.listNotebooks();
console.timeEnd('First notebooks call');
// Second call - cache hit
console.log('\nSecond request (cache hit):');
console.time('Second notebooks call');
await client.listNotebooks();
console.timeEnd('Second notebooks call');
// Search with caching
console.log('\nSearch (cache miss):');
console.time('First search');
await client.search('meeting notes');
console.timeEnd('First search');
console.log('\nSearch (cache hit):');
console.time('Second search');
await client.search('meeting notes');
console.timeEnd('Second search');
// Print stats
console.log('\nPerformance Stats:', monitor.getStats());
redis.quit();
}
main().catch(console.error);| Optimization | Impact | When to Use |
|---|---|---|
| Cache notebooks | High | Always (rarely change) |
| Cache search results | Medium | Repeated searches |
| Lazy load resources | High | Large attachments |
| Request batching | Medium | Bulk operations |
| Skip content flag | High | Listing notes |
For cost optimization, see evernote-cost-tuning.