11import { db } from '@sim/db'
22import { idempotencyKey } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
4- import { and , eq , lt } from 'drizzle-orm'
4+ import { and , count , inArray , like , lt , max , min , sql } from 'drizzle-orm'
55
66const logger = createLogger ( 'IdempotencyCleanup' )
77
@@ -19,7 +19,8 @@ export interface CleanupOptions {
1919 batchSize ?: number
2020
2121 /**
22- * Specific namespace to clean up, or undefined to clean all namespaces
22+ * Specific namespace prefix to clean up (e.g., 'webhook', 'polling')
23+ * Keys are prefixed with namespace, so this filters by key prefix
2324 */
2425 namespace ?: string
2526}
@@ -53,13 +54,17 @@ export async function cleanupExpiredIdempotencyKeys(
5354
5455 while ( hasMore ) {
5556 try {
57+ // Build where condition - filter by cutoff date and optionally by namespace prefix
5658 const whereCondition = namespace
57- ? and ( lt ( idempotencyKey . createdAt , cutoffDate ) , eq ( idempotencyKey . namespace , namespace ) )
59+ ? and (
60+ lt ( idempotencyKey . createdAt , cutoffDate ) ,
61+ like ( idempotencyKey . key , `${ namespace } :%` )
62+ )
5863 : lt ( idempotencyKey . createdAt , cutoffDate )
5964
60- // First, find IDs to delete with limit
65+ // Find keys to delete with limit
6166 const toDelete = await db
62- . select ( { key : idempotencyKey . key , namespace : idempotencyKey . namespace } )
67+ . select ( { key : idempotencyKey . key } )
6368 . from ( idempotencyKey )
6469 . where ( whereCondition )
6570 . limit ( batchSize )
@@ -68,14 +73,13 @@ export async function cleanupExpiredIdempotencyKeys(
6873 break
6974 }
7075
71- // Delete the found records
76+ // Delete the found records by key
7277 const deleteResult = await db
7378 . delete ( idempotencyKey )
7479 . where (
75- and (
76- ...toDelete . map ( ( item ) =>
77- and ( eq ( idempotencyKey . key , item . key ) , eq ( idempotencyKey . namespace , item . namespace ) )
78- )
80+ inArray (
81+ idempotencyKey . key ,
82+ toDelete . map ( ( item ) => item . key )
7983 )
8084 )
8185 . returning ( { key : idempotencyKey . key } )
@@ -126,6 +130,7 @@ export async function cleanupExpiredIdempotencyKeys(
126130
127131/**
128132 * Get statistics about idempotency key usage
133+ * Uses SQL aggregations to avoid loading all keys into memory
129134 */
130135export async function getIdempotencyKeyStats ( ) : Promise < {
131136 totalKeys : number
@@ -134,34 +139,35 @@ export async function getIdempotencyKeyStats(): Promise<{
134139 newestKey : Date | null
135140} > {
136141 try {
137- const allKeys = await db
142+ // Get total count and date range in a single query
143+ const [ statsResult ] = await db
138144 . select ( {
139- namespace : idempotencyKey . namespace ,
140- createdAt : idempotencyKey . createdAt ,
145+ totalKeys : count ( ) ,
146+ oldestKey : min ( idempotencyKey . createdAt ) ,
147+ newestKey : max ( idempotencyKey . createdAt ) ,
141148 } )
142149 . from ( idempotencyKey )
143150
144- const totalKeys = allKeys . length
145- const keysByNamespace : Record < string , number > = { }
146- let oldestKey : Date | null = null
147- let newestKey : Date | null = null
148-
149- for ( const key of allKeys ) {
150- keysByNamespace [ key . namespace ] = ( keysByNamespace [ key . namespace ] || 0 ) + 1
151+ // Get counts by namespace prefix using SQL substring
152+ // Extracts everything before the first ':' as the namespace
153+ const namespaceStats = await db
154+ . select ( {
155+ namespace : sql < string > `split_part(${ idempotencyKey . key } , ':', 1)` . as ( 'namespace' ) ,
156+ count : count ( ) ,
157+ } )
158+ . from ( idempotencyKey )
159+ . groupBy ( sql `split_part(${ idempotencyKey . key } , ':', 1)` )
151160
152- if ( ! oldestKey || key . createdAt < oldestKey ) {
153- oldestKey = key . createdAt
154- }
155- if ( ! newestKey || key . createdAt > newestKey ) {
156- newestKey = key . createdAt
157- }
161+ const keysByNamespace : Record < string , number > = { }
162+ for ( const row of namespaceStats ) {
163+ keysByNamespace [ row . namespace || 'unknown' ] = row . count
158164 }
159165
160166 return {
161- totalKeys,
167+ totalKeys : statsResult ?. totalKeys ?? 0 ,
162168 keysByNamespace,
163- oldestKey,
164- newestKey,
169+ oldestKey : statsResult ?. oldestKey ?? null ,
170+ newestKey : statsResult ?. newestKey ?? null ,
165171 }
166172 } catch ( error ) {
167173 logger . error ( 'Failed to get idempotency key stats:' , error )
0 commit comments