-
Notifications
You must be signed in to change notification settings - Fork 3
Extract generic MethodCacheProxy from ghc.ts and slackCached.ts #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR extracts a reusable MethodCacheProxy class to eliminate code duplication between the GitHub and Slack cached clients. The implementation provides transparent method caching for any object with customizable cache key generation and conditional caching.
Key changes:
- Created a generic
MethodCacheProxyutility class with comprehensive TypeScript support - Refactored both
ghc.tsandslackCached.tsto use the new abstraction - Added comprehensive test coverage with 13 test cases
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/MethodCacheProxy.ts | New generic cache proxy implementation |
| src/utils/MethodCacheProxy.spec.ts | Comprehensive test suite for the cache proxy |
| src/slack/slackCached.ts | Refactored to use MethodCacheProxy with custom Slack cache keys |
| src/ghc.ts | Refactored to use MethodCacheProxy with custom GitHub cache keys |
| src/MethodCacheProxy.ts | Duplicate implementation with additional shouldCache functionality |
| src/MethodCacheProxy.spec.ts | Different test implementation using vitest instead of jest |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| import type Keyv from "keyv"; | ||
|
|
||
| export interface MethodCacheProxyOptions<T extends object> { | ||
| store: Keyv; | ||
| root: T; | ||
| getKey?: (path: (string | symbol)[], args: any[]) => string; | ||
| shouldCache?: (path: (string | symbol)[], args: any[], result: any, error?: Error) => boolean; | ||
| } | ||
|
|
||
| type DeepAsyncWrapper<T> = { | ||
| [K in keyof T]: T[K] extends (...args: any[]) => Promise<any> | ||
| ? T[K] | ||
| : T[K] extends (...args: any[]) => any | ||
| ? (...args: Parameters<T[K]>) => Promise<ReturnType<T[K]>> | ||
| : T[K] extends object | ||
| ? DeepAsyncWrapper<T[K]> | ||
| : T[K]; | ||
| }; | ||
|
|
||
| export class MethodCacheProxy<T extends object> { | ||
| private proxy: DeepAsyncWrapper<T>; | ||
| private store: Keyv; | ||
| private getKey: (path: (string | symbol)[], args: any[]) => string; | ||
| private shouldCache: (path: (string | symbol)[], args: any[], result: any, error?: Error) => boolean; | ||
|
|
||
| constructor(options: MethodCacheProxyOptions<T>) { | ||
| this.store = options.store; | ||
| this.getKey = options.getKey || this.defaultGetKey; | ||
| this.shouldCache = options.shouldCache || this.defaultShouldCache; | ||
| this.proxy = this.createProxy(options.root) as DeepAsyncWrapper<T>; | ||
| } | ||
|
|
||
| private defaultGetKey(path: (string | symbol)[], args: any[]): string { | ||
| // Default implementation similar to existing code | ||
| const pathStr = path.map(p => p.toString()).join("."); | ||
| const argsStr = JSON.stringify(args); | ||
| return `${pathStr}(${argsStr})`; | ||
| } | ||
|
|
||
| private defaultShouldCache(path: (string | symbol)[], args: any[], result: any, error?: Error): boolean { | ||
| // Don't cache if there's an error | ||
| if (error) return false; | ||
| // Cache successful results by default | ||
| return true; | ||
| } | ||
|
|
||
| private createProxy(target: any, basePath: (string | symbol)[] = []): any { | ||
| return new Proxy(target, { | ||
| get: (obj, prop) => { | ||
| const value = obj[prop]; | ||
|
|
||
| if (typeof value === "function") { | ||
| return async (...args: any[]) => { | ||
| const path = [...basePath, prop]; | ||
| const cacheKey = this.getKey(path, args); | ||
|
|
||
| // Try to get from cache first | ||
| const cached = await this.store.get(cacheKey); | ||
| if (cached !== undefined) { | ||
| return cached; | ||
| } | ||
|
|
||
| // Call the original function | ||
| let result: any; | ||
| let error: Error | undefined; | ||
|
|
||
| try { | ||
| result = await value.apply(obj, args); | ||
| } catch (e) { | ||
| error = e as Error; | ||
| throw e; | ||
| } finally { | ||
| // Cache the result if shouldCache returns true | ||
| if (!error && this.shouldCache(path, args, result, error)) { | ||
| await this.store.set(cacheKey, result); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| }; | ||
| } else if (typeof value === "object" && value !== null) { | ||
| // Recursively wrap nested objects | ||
| return this.createProxy(value, [...basePath, prop]); | ||
| } | ||
|
|
||
| return value; | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| getProxy(): DeepAsyncWrapper<T> { | ||
| return this.proxy; | ||
| } | ||
|
|
||
| async clear(): Promise<void> { | ||
| await this.store.clear(); | ||
| } | ||
|
|
||
| async delete(key: string): Promise<boolean> { | ||
| return this.store.delete(key); | ||
| } | ||
|
|
||
| async has(key: string): Promise<boolean> { | ||
| return this.store.has(key); | ||
| } | ||
|
|
||
| async get(key: string): Promise<any> { | ||
| return this.store.get(key); | ||
| } | ||
|
|
||
| async set(key: string, value: any, ttl?: number): Promise<boolean> { | ||
| return this.store.set(key, value, ttl); | ||
| } | ||
| } No newline at end of file |
Copilot
AI
Sep 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file appears to be a duplicate of src/utils/MethodCacheProxy.ts with additional shouldCache functionality. Having two implementations of the same class in different locations creates maintainability issues and confusion.
| import type Keyv from "keyv"; | |
| export interface MethodCacheProxyOptions<T extends object> { | |
| store: Keyv; | |
| root: T; | |
| getKey?: (path: (string | symbol)[], args: any[]) => string; | |
| shouldCache?: (path: (string | symbol)[], args: any[], result: any, error?: Error) => boolean; | |
| } | |
| type DeepAsyncWrapper<T> = { | |
| [K in keyof T]: T[K] extends (...args: any[]) => Promise<any> | |
| ? T[K] | |
| : T[K] extends (...args: any[]) => any | |
| ? (...args: Parameters<T[K]>) => Promise<ReturnType<T[K]>> | |
| : T[K] extends object | |
| ? DeepAsyncWrapper<T[K]> | |
| : T[K]; | |
| }; | |
| export class MethodCacheProxy<T extends object> { | |
| private proxy: DeepAsyncWrapper<T>; | |
| private store: Keyv; | |
| private getKey: (path: (string | symbol)[], args: any[]) => string; | |
| private shouldCache: (path: (string | symbol)[], args: any[], result: any, error?: Error) => boolean; | |
| constructor(options: MethodCacheProxyOptions<T>) { | |
| this.store = options.store; | |
| this.getKey = options.getKey || this.defaultGetKey; | |
| this.shouldCache = options.shouldCache || this.defaultShouldCache; | |
| this.proxy = this.createProxy(options.root) as DeepAsyncWrapper<T>; | |
| } | |
| private defaultGetKey(path: (string | symbol)[], args: any[]): string { | |
| // Default implementation similar to existing code | |
| const pathStr = path.map(p => p.toString()).join("."); | |
| const argsStr = JSON.stringify(args); | |
| return `${pathStr}(${argsStr})`; | |
| } | |
| private defaultShouldCache(path: (string | symbol)[], args: any[], result: any, error?: Error): boolean { | |
| // Don't cache if there's an error | |
| if (error) return false; | |
| // Cache successful results by default | |
| return true; | |
| } | |
| private createProxy(target: any, basePath: (string | symbol)[] = []): any { | |
| return new Proxy(target, { | |
| get: (obj, prop) => { | |
| const value = obj[prop]; | |
| if (typeof value === "function") { | |
| return async (...args: any[]) => { | |
| const path = [...basePath, prop]; | |
| const cacheKey = this.getKey(path, args); | |
| // Try to get from cache first | |
| const cached = await this.store.get(cacheKey); | |
| if (cached !== undefined) { | |
| return cached; | |
| } | |
| // Call the original function | |
| let result: any; | |
| let error: Error | undefined; | |
| try { | |
| result = await value.apply(obj, args); | |
| } catch (e) { | |
| error = e as Error; | |
| throw e; | |
| } finally { | |
| // Cache the result if shouldCache returns true | |
| if (!error && this.shouldCache(path, args, result, error)) { | |
| await this.store.set(cacheKey, result); | |
| } | |
| } | |
| return result; | |
| }; | |
| } else if (typeof value === "object" && value !== null) { | |
| // Recursively wrap nested objects | |
| return this.createProxy(value, [...basePath, prop]); | |
| } | |
| return value; | |
| }, | |
| }); | |
| } | |
| getProxy(): DeepAsyncWrapper<T> { | |
| return this.proxy; | |
| } | |
| async clear(): Promise<void> { | |
| await this.store.clear(); | |
| } | |
| async delete(key: string): Promise<boolean> { | |
| return this.store.delete(key); | |
| } | |
| async has(key: string): Promise<boolean> { | |
| return this.store.has(key); | |
| } | |
| async get(key: string): Promise<any> { | |
| return this.store.get(key); | |
| } | |
| async set(key: string, value: any, ttl?: number): Promise<boolean> { | |
| return this.store.set(key, value, ttl); | |
| } | |
| } | |
| export * from "./utils/MethodCacheProxy"; |
| import Keyv from "keyv"; | ||
| import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; | ||
| import { MethodCacheProxy } from "./MethodCacheProxy"; |
Copilot
AI
Sep 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test file is importing from ./MethodCacheProxy while there's also a test file at src/utils/MethodCacheProxy.spec.ts. Having duplicate test suites for the same functionality creates maintenance overhead and potential inconsistencies.
| async delete(path: (string | symbol)[], args: any[]): Promise<boolean> { | ||
| const cacheKey = this.getKey(path, args); |
Copilot
AI
Sep 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delete method signature differs from the duplicate implementation in src/MethodCacheProxy.ts which accepts a direct key string. This inconsistency could confuse developers about which interface to use.
| async delete(path: (string | symbol)[], args: any[]): Promise<boolean> { | |
| const cacheKey = this.getKey(path, args); | |
| async delete(key: string): Promise<boolean>; | |
| async delete(path: (string | symbol)[], args: any[]): Promise<boolean>; | |
| async delete(arg1: string | (string | symbol)[], arg2?: any[]): Promise<boolean> { | |
| let cacheKey: string; | |
| if (typeof arg1 === "string") { | |
| cacheKey = arg1; | |
| } else { | |
| cacheKey = this.getKey(arg1, arg2 ?? []); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
|
extracting to external package - method-cache-proxy - npm |
Review ResponseThe duplicate MethodCacheProxy files issue has been identified. However, after reviewing the codebase changes:
RecommendationGiven that the main branch has moved away from the MethodCacheProxy pattern and the author has published this as an external package, I recommend closing this PR as the codebase has taken a different direction. If we want to use the external package approach in the future, we should create a new PR that:
🤖 Generated with Claude Code |
- Created a generic MethodCacheProxy class that accepts a Keyv store, root object, and key generation function - Refactored ghc.ts to use MethodCacheProxy instead of custom proxy implementation - Refactored slackCached.ts to use MethodCacheProxy for consistency - Added comprehensive test suite for MethodCacheProxy with 13 test cases - Improved code reusability and maintainability by extracting common caching logic The new MethodCacheProxy utility provides: - Transparent method result caching for any object - Support for nested object methods - Automatic async wrapping of sync methods - Custom cache key generation support - Cache management methods (clear, delete) - Proper error handling (doesn't cache undefined/errors) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
c97d384 to
c86e459
Compare
WIP:
extracting complex logic to external package to reduce complexity in this repo
Summary
MethodCacheProxyclass that provides transparent method caching for any objectghc.ts(GitHub client) andslackCached.ts(Slack client) to use the new abstractionChanges
src/utils/MethodCacheProxy.ts- Generic cache proxy implementationsrc/utils/MethodCacheProxy.spec.ts- Comprehensive test suitesrc/ghc.tsto use MethodCacheProxysrc/slack/slackCached.tsto use MethodCacheProxyKey Features of MethodCacheProxy
getKeyfunctionshouldCachefunction for conditional cachingTesting
All tests pass successfully (13 tests with 48 assertions).
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]