1+ import crypto from 'node:crypto'
12import { createLogger } from '@sim/logger'
23import { getRedisClient } from '@/lib/core/config/redis'
4+ import type { PaginatedCacheStorageAdapter } from '@/lib/paginated-cache/adapter'
35import { RedisPaginatedCache } from '@/lib/paginated-cache/redis-cache'
4- import { isPaginatedCacheReference } from '@/lib/paginated-cache/types'
56import type { PaginatedCacheReference , ToolPaginationConfig } from '@/lib/paginated-cache/types'
7+ import { isPaginatedCacheReference } from '@/lib/paginated-cache/types'
68import type { ToolResponse } from '@/tools/types'
79
810const logger = createLogger ( 'Paginator' )
911
10- const DEFAULT_MAX_PAGES = 100
12+ const DEFAULT_MAX_PAGES = 10_000
1113
1214interface AutoPaginateOptions {
1315 initialResult : ToolResponse
@@ -23,8 +25,14 @@ interface AutoPaginateOptions {
2325}
2426
2527export async function autoPaginate ( options : AutoPaginateOptions ) : Promise < ToolResponse > {
26- const { initialResult, params, paginationConfig : config , executeTool, toolId, executionId } =
27- options
28+ const {
29+ initialResult,
30+ params,
31+ paginationConfig : config ,
32+ executeTool,
33+ toolId,
34+ executionId,
35+ } = options
2836 const maxPages = config . maxPages ?? DEFAULT_MAX_PAGES
2937
3038 const redis = getRedisClient ( )
@@ -33,7 +41,7 @@ export async function autoPaginate(options: AutoPaginateOptions): Promise<ToolRe
3341 }
3442
3543 const cache = new RedisPaginatedCache ( redis )
36- const cacheId = `${ executionId } :${ toolId } :${ config . pageField } :${ Date . now ( ) } `
44+ const cacheId = `${ executionId } :${ toolId } :${ config . pageField } :${ crypto . randomUUID ( ) } `
3745
3846 let totalItems = 0
3947 let pageIndex = 0
@@ -78,11 +86,18 @@ export async function autoPaginate(options: AutoPaginateOptions): Promise<ToolRe
7886
7987 logger . info ( 'Auto-pagination complete' , { cacheId, totalPages, totalItems, toolId } )
8088
89+ const lastMeta = ( lastOutput as Record < string , unknown > ) . metadata
90+ const patchedMetadata =
91+ typeof lastMeta === 'object' && lastMeta !== null
92+ ? { ...lastMeta , total_returned : totalItems }
93+ : lastMeta
94+
8195 return {
8296 ...initialResult ,
8397 output : {
8498 ...lastOutput ,
8599 [ config . pageField ] : reference ,
100+ ...( patchedMetadata !== undefined && { metadata : patchedMetadata } ) ,
86101 } ,
87102 }
88103}
@@ -97,7 +112,14 @@ export async function hydrateCacheReferences(
97112 if ( ! containsCacheReference ( inputs ) ) {
98113 return inputs
99114 }
100- return ( await deepHydrate ( inputs ) ) as Record < string , unknown >
115+
116+ const redis = getRedisClient ( )
117+ if ( ! redis ) {
118+ throw new Error ( 'Redis is required to hydrate paginated cache references but is not available' )
119+ }
120+
121+ const adapter = new RedisPaginatedCache ( redis )
122+ return ( await deepHydrate ( inputs , adapter ) ) as Record < string , unknown >
101123}
102124
103125function containsCacheReference ( value : unknown ) : boolean {
@@ -109,37 +131,35 @@ function containsCacheReference(value: unknown): boolean {
109131 return false
110132}
111133
112- async function deepHydrate ( value : unknown ) : Promise < unknown > {
134+ async function deepHydrate (
135+ value : unknown ,
136+ adapter : PaginatedCacheStorageAdapter
137+ ) : Promise < unknown > {
113138 if ( isPaginatedCacheReference ( value ) ) {
114- return hydrateReference ( value )
139+ return hydrateReference ( value , adapter )
115140 }
116141
117142 if ( Array . isArray ( value ) ) {
118- return Promise . all ( value . map ( deepHydrate ) )
143+ return Promise . all ( value . map ( ( v ) => deepHydrate ( v , adapter ) ) )
119144 }
120145
121146 if ( typeof value === 'object' && value !== null ) {
122147 const entries = Object . entries ( value as Record < string , unknown > )
123148 const hydrated : Record < string , unknown > = { }
124149 for ( const [ key , val ] of entries ) {
125- hydrated [ key ] = await deepHydrate ( val )
150+ hydrated [ key ] = await deepHydrate ( val , adapter )
126151 }
127152 return hydrated
128153 }
129154
130155 return value
131156}
132157
133- async function hydrateReference ( ref : PaginatedCacheReference ) : Promise < unknown [ ] > {
134- const redis = getRedisClient ( )
135- if ( ! redis ) {
136- throw new Error (
137- `Redis is required to hydrate paginated cache reference (cacheId: ${ ref . cacheId } ) but is not available`
138- )
139- }
140-
141- const cache = new RedisPaginatedCache ( redis )
142- const pages = await cache . getAllPages ( ref . cacheId , ref . totalPages )
158+ async function hydrateReference (
159+ ref : PaginatedCacheReference ,
160+ adapter : PaginatedCacheStorageAdapter
161+ ) : Promise < unknown [ ] > {
162+ const pages = await adapter . getAllPages ( ref . cacheId , ref . totalPages )
143163
144164 const items : unknown [ ] = [ ]
145165 for ( const page of pages ) {
@@ -165,21 +185,23 @@ export async function cleanupPaginatedCache(executionId: string): Promise<void>
165185 return
166186 }
167187
168- const pattern = `pagcache:* ${ executionId } :*`
188+ const patterns = [ `pagcache:page: ${ executionId } :*` , `pagcache:meta: ${ executionId } :*`]
169189
170190 try {
171- let cursor = '0'
172191 let deletedCount = 0
173192
174- do {
175- const [ nextCursor , keys ] = await redis . scan ( cursor , 'MATCH' , pattern , 'COUNT' , 100 )
176- cursor = nextCursor
177-
178- if ( keys . length > 0 ) {
179- await redis . del ( ...keys )
180- deletedCount += keys . length
181- }
182- } while ( cursor !== '0' )
193+ for ( const pattern of patterns ) {
194+ let cursor = '0'
195+ do {
196+ const [ nextCursor , keys ] = await redis . scan ( cursor , 'MATCH' , pattern , 'COUNT' , 100 )
197+ cursor = nextCursor
198+
199+ if ( keys . length > 0 ) {
200+ await redis . del ( ...keys )
201+ deletedCount += keys . length
202+ }
203+ } while ( cursor !== '0' )
204+ }
183205
184206 if ( deletedCount > 0 ) {
185207 logger . info ( `Cleaned up ${ deletedCount } paginated cache entries for execution ${ executionId } ` )
0 commit comments