Skip to content

Commit 6580442

Browse files
committed
feat(cache): add per-endpoint TTL configuration for API caching
Add ability to configure different cache TTLs for different endpoints: - Support both single TTL (backward compatible) and per-endpoint config - Add recommended TTLs: organizations (30 min), quota (10 min) - Implement cache instance pooling to support multiple TTL values - Maintain backward compatibility with existing cacheTtl number param This reduces unnecessary API calls for rarely-changing data like organization lists while keeping more dynamic data like quotas fresher. Example usage: ```typescript const sdk = new SocketSdk(token, { cache: true, cacheTtl: { default: 5 * 60 * 1000, // 5 minutes organizations: 30 * 60 * 1000, // 30 minutes quota: 10 * 60 * 1000 // 10 minutes } }); ```
1 parent 3b6cae9 commit 6580442

File tree

3 files changed

+112
-6
lines changed

3 files changed

+112
-6
lines changed

src/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ export const DEFAULT_RETRIES = 3
2626
// Default delay before first retry (milliseconds)
2727
export const DEFAULT_RETRY_DELAY = 1000
2828

29+
// Default cache TTL (5 minutes)
30+
export const DEFAULT_CACHE_TTL = 5 * 60 * 1000
31+
32+
// Recommended cache TTL for organizations endpoint (30 minutes)
33+
// Organizations list rarely changes - only when joining/leaving orgs.
34+
export const RECOMMENDED_CACHE_TTL_ORGANIZATIONS = 30 * 60 * 1000
35+
36+
// Recommended cache TTL for quota endpoint (10 minutes)
37+
// Quota changes incrementally and doesn't need real-time accuracy.
38+
export const RECOMMENDED_CACHE_TTL_QUOTA = 10 * 60 * 1000
39+
2940
// Maximum timeout for HTTP requests (5 minutes)
3041
export const MAX_HTTP_TIMEOUT = 5 * 60 * 1000
3142

src/socket-sdk-class.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { urlSearchParamAsBoolean } from '@socketsecurity/lib/url'
2020
const abortSignal = getAbortSignal()
2121

2222
import {
23+
DEFAULT_CACHE_TTL,
2324
DEFAULT_HTTP_TIMEOUT,
2425
DEFAULT_RETRIES,
2526
DEFAULT_RETRY_DELAY,
@@ -116,6 +117,8 @@ export class SocketSdk {
116117
readonly #apiToken: string
117118
readonly #baseUrl: string
118119
readonly #cache: TtlCache | undefined
120+
readonly #cacheByTtl: Map<number, TtlCache>
121+
readonly #cacheTtlConfig: SocketSdkOptions['cacheTtl']
119122
readonly #hooks: SocketSdkOptions['hooks']
120123
readonly #onFileValidation: FileValidationCallback | undefined
121124
readonly #reqOptions: RequestOptions
@@ -146,7 +149,7 @@ export class SocketSdk {
146149
agent: agentOrObj,
147150
baseUrl = 'https://api.socket.dev/v0/',
148151
cache = false,
149-
cacheTtl = 5 * 60 * 1000,
152+
cacheTtl,
150153
hooks,
151154
onFileValidation,
152155
retries = DEFAULT_RETRIES,
@@ -180,13 +183,22 @@ export class SocketSdk {
180183
) as Agent | undefined
181184
this.#apiToken = trimmedToken
182185
this.#baseUrl = normalizeBaseUrl(baseUrl)
186+
this.#cacheTtlConfig = cacheTtl
187+
// For backward compatibility, if cacheTtl is a number, use it as default TTL.
188+
// If it's an object, use the default property or fallback to DEFAULT_CACHE_TTL.
189+
const defaultTtl =
190+
typeof cacheTtl === 'number'
191+
? cacheTtl
192+
: (cacheTtl?.default ?? DEFAULT_CACHE_TTL)
183193
this.#cache = cache
184194
? createTtlCache({
185195
memoize: true,
186196
prefix: 'socket-sdk',
187-
ttl: cacheTtl,
197+
ttl: defaultTtl,
188198
})
189199
: /* c8 ignore next - cache disabled by default */ undefined
200+
// Map of TTL values to cache instances for per-endpoint caching.
201+
this.#cacheByTtl = new Map()
190202
this.#hooks = hooks
191203
this.#onFileValidation = onFileValidation
192204
this.#retries = retries
@@ -366,18 +378,74 @@ export class SocketSdk {
366378
return result
367379
}
368380

381+
/**
382+
* Get the TTL for a specific endpoint.
383+
* Returns endpoint-specific TTL if configured, otherwise returns default TTL.
384+
*/
385+
#getTtlForEndpoint(endpoint: string): number | undefined {
386+
const cacheTtl = this.#cacheTtlConfig
387+
if (typeof cacheTtl === 'number') {
388+
return cacheTtl
389+
}
390+
if (cacheTtl && typeof cacheTtl === 'object') {
391+
// Check for endpoint-specific TTL first.
392+
const endpointTtl = (cacheTtl as any)[endpoint]
393+
if (typeof endpointTtl === 'number') {
394+
return endpointTtl
395+
}
396+
// Fall back to default.
397+
return cacheTtl.default
398+
}
399+
return undefined
400+
}
401+
402+
/**
403+
* Get or create a cache instance with the specified TTL.
404+
* Reuses existing cache instances to avoid creating duplicates.
405+
*/
406+
#getCacheForTtl(ttl: number): TtlCache {
407+
let cache = this.#cacheByTtl.get(ttl)
408+
if (!cache) {
409+
cache = createTtlCache({
410+
memoize: true,
411+
prefix: 'socket-sdk',
412+
ttl,
413+
})
414+
this.#cacheByTtl.set(ttl, cache)
415+
}
416+
return cache
417+
}
418+
369419
/**
370420
* Execute a GET request with optional caching.
371421
* Internal method for handling cached GET requests with retry logic.
422+
* Supports per-endpoint TTL configuration.
372423
*/
373-
async #getCached<T>(cacheKey: string, fetcher: () => Promise<T>): Promise<T> {
424+
async #getCached<T>(
425+
cacheKey: string,
426+
fetcher: () => Promise<T>,
427+
endpointName?: string | undefined,
428+
): Promise<T> {
374429
// If caching is disabled, just execute the request.
375430
if (!this.#cache) {
376431
return await this.#executeWithRetry(fetcher)
377432
}
378433

434+
// Get endpoint-specific TTL if provided.
435+
const endpointTtl = endpointName
436+
? this.#getTtlForEndpoint(endpointName)
437+
: undefined
438+
439+
// Select the appropriate cache instance.
440+
// If endpoint has custom TTL, get/create cache for that TTL.
441+
// Otherwise use the default cache.
442+
const cacheToUse =
443+
endpointTtl !== undefined
444+
? this.#getCacheForTtl(endpointTtl)
445+
: this.#cache
446+
379447
// Use cache with retry logic.
380-
return await this.#cache.getOrFetch(cacheKey, async () => {
448+
return await cacheToUse.getOrFetch(cacheKey, async () => {
381449
return await this.#executeWithRetry(fetcher)
382450
})
383451
}
@@ -1789,6 +1857,7 @@ export class SocketSdk {
17891857
this.#hooks,
17901858
),
17911859
),
1860+
'organizations',
17921861
)
17931862
return {
17941863
cause: undefined,
@@ -2370,6 +2439,7 @@ export class SocketSdk {
23702439
this.#hooks,
23712440
),
23722441
),
2442+
'quota',
23732443
)
23742444
return this.#handleApiSuccess<'getQuota'>(data)
23752445
/* c8 ignore start - Standard API error handling, tested via public method error cases */

src/types.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,14 +302,39 @@ export interface SocketSdkOptions {
302302
baseUrl?: string | undefined
303303
/**
304304
* Enable TTL caching for API responses (default: false).
305-
* When enabled, GET requests are cached with a 5-minute TTL.
305+
* When enabled, GET requests are cached with configurable TTLs.
306+
* Only applies to listOrganizations() and getQuota() methods.
306307
*/
307308
cache?: boolean | undefined
308309
/**
309310
* Cache TTL in milliseconds (default: 300_000 = 5 minutes).
310311
* Only used when cache is enabled.
312+
* Can be a single number for all endpoints or an object for per-endpoint TTLs.
313+
*
314+
* Recommended TTLs by endpoint:
315+
* - organizations: 30 minutes (rarely changes)
316+
* - quota: 10 minutes (changes incrementally)
317+
*
318+
* @example
319+
* // Single TTL for all endpoints.
320+
* cacheTtl: 15 * 60 * 1000 // 15 minutes
321+
*
322+
* @example
323+
* // Per-endpoint TTLs with recommended values.
324+
* cacheTtl: {
325+
* default: 5 * 60 * 1000, // 5 minutes default
326+
* organizations: 30 * 60 * 1000, // 30 minutes (recommended)
327+
* quota: 10 * 60 * 1000 // 10 minutes (recommended)
328+
* }
311329
*/
312-
cacheTtl?: number | undefined
330+
cacheTtl?:
331+
| number
332+
| {
333+
default?: number | undefined
334+
organizations?: number | undefined
335+
quota?: number | undefined
336+
}
337+
| undefined
313338
/**
314339
* Callback for file validation events.
315340
* Called when any file-upload method detects unreadable files:

0 commit comments

Comments
 (0)