@@ -15,6 +15,7 @@ import Crypto from 'crypto';
15
15
import { HapiRequest , HapiServer } from "../types" ;
16
16
import { AdapterFormModel } from "../plugins/engine/models" ;
17
17
import Boom from "boom" ;
18
+ import { PreAwardApiService , PublishedFormResponse } from "./PreAwardApiService" ;
18
19
19
20
const partition = "cache" ;
20
21
const LOGGER_DATA = {
@@ -34,7 +35,7 @@ if (process.env.FORM_RUNNER_ADAPTER_REDIS_INSTANCE_URI) {
34
35
redisUri = process . env . FORM_RUNNER_ADAPTER_REDIS_INSTANCE_URI ;
35
36
}
36
37
37
- export const FORMS_KEY_PREFIX = "forms:cache: "
38
+ export const FORMS_KEY_PREFIX = "forms"
38
39
39
40
enum ADDITIONAL_IDENTIFIER {
40
41
Confirmation = ":confirmation" ,
@@ -57,11 +58,13 @@ const createRedisClient = (): Redis | null => {
57
58
} ;
58
59
59
60
export class AdapterCacheService extends CacheService {
61
+ private apiService : PreAwardApiService ;
60
62
private formStorage : Redis | any ;
61
63
62
64
constructor ( server : HapiServer ) {
63
65
//@ts -ignore
64
66
super ( server ) ;
67
+ this . apiService = server . services ( [ ] ) . preAwardApiService ;
65
68
const redisClient = this . getRedisClient ( ) ;
66
69
if ( redisClient ) {
67
70
this . formStorage = redisClient ;
@@ -134,19 +137,55 @@ export class AdapterCacheService extends CacheService {
134
137
}
135
138
136
139
/**
137
- * handling form configuration's in a redis cache to easily distribute them among instance
138
- * * If given fom id is not available, it will generate a hash based on the configuration, And it will be saved in redis cache
139
- * * If hash is change then updating the redis
140
- * @param formId form id
141
- * @param configuration form definition configurations
142
- * @param server server object
140
+ * Validates cached form against Pre-Award API.
141
+ */
142
+ private async validateCachedForm ( formId : string , cachedHash : string , request : HapiRequest ) : Promise < boolean > {
143
+ try {
144
+ const currentHash = await this . apiService . getFormHash ( formId , request ) ;
145
+ return currentHash === cachedHash ;
146
+ } catch ( error ) {
147
+ // If we can't validate, assume cache is valid
148
+ request . logger . warn ( {
149
+ ...LOGGER_DATA ,
150
+ message : `Could not validate cache for form ${ formId } , using cached version`
151
+ } ) ;
152
+ return true ;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Fetches form from Pre-Award API and caches it.
158
+ */
159
+ private async fetchAndCacheForm ( formId : string , request : HapiRequest ) : Promise < PublishedFormResponse | null > {
160
+ try {
161
+ const apiResponse = await this . apiService . getPublishedForm ( formId , request ) ;
162
+ if ( ! apiResponse ) return null ;
163
+ const formsCacheKey = `${ FORMS_KEY_PREFIX } :${ formId } ` ;
164
+ await this . formStorage . set ( formsCacheKey , JSON . stringify ( apiResponse ) ) ;
165
+ request . logger . info ( {
166
+ ...LOGGER_DATA ,
167
+ message : `Cached form ${ formId } from Pre-Award API`
168
+ } ) ;
169
+ return apiResponse as PublishedFormResponse ;
170
+ } catch ( error ) {
171
+ request . logger . error ( {
172
+ ...LOGGER_DATA ,
173
+ message : `Failed to fetch form ${ formId } ` ,
174
+ error : error
175
+ } ) ;
176
+ return null ;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * This is used to ensure unit tests can populate the cache
143
182
*/
144
183
async setFormConfiguration ( formId : string , configuration : any ) : Promise < void > {
145
184
if ( ! formId || ! configuration ) return ;
146
185
const hashValue = Crypto . createHash ( 'sha256' )
147
186
. update ( JSON . stringify ( configuration ) )
148
187
. digest ( 'hex' ) ;
149
- const key = `${ FORMS_KEY_PREFIX } ${ formId } ` ;
188
+ const key = `${ FORMS_KEY_PREFIX } : ${ formId } ` ;
150
189
try {
151
190
const existingConfigString = await this . formStorage . get ( key ) ;
152
191
if ( existingConfigString === null ) {
@@ -175,25 +214,80 @@ export class AdapterCacheService extends CacheService {
175
214
}
176
215
}
177
216
217
+ /**
218
+ * Retrieves form configuration, either from cache or Pre-Award API.
219
+ */
178
220
async getFormAdapterModel ( formId : string , request : HapiRequest ) : Promise < AdapterFormModel > {
179
221
const { translationLoaderService} = request . services ( [ ] ) ;
180
222
const translations = translationLoaderService . getTranslations ( ) ;
181
- const jsonDataString = await this . formStorage . get ( `${ FORMS_KEY_PREFIX } ${ formId } ` ) ;
223
+ const formCacheKey = `${ FORMS_KEY_PREFIX } :${ formId } ` ;
224
+ const jsonDataString = await this . formStorage . get ( formCacheKey ) ;
225
+ // We use a separate key to track if we've validated that this form is up-to-date in this session
226
+ // We use yar.id instead of form_session_identifier as form_session_identifier is not present in the first request
227
+ const formSessionCacheKey = `${ formCacheKey } :${ request . yar . id } ` ;
228
+ const sessionValidated = await this . formStorage . get ( formSessionCacheKey ) ;
229
+ let configObj = null ;
182
230
if ( jsonDataString !== null ) {
183
- const configObj = JSON . parse ( jsonDataString ) ;
184
- return new AdapterFormModel ( configObj . configuration , {
185
- basePath : configObj . id ? configObj . id : formId ,
186
- hash : configObj . hash ,
187
- previewMode : true ,
188
- translationEn : translations . en ,
189
- translationCy : translations . cy
231
+ // Cache hit
232
+ request . logger . debug ( {
233
+ ...LOGGER_DATA ,
234
+ message : `Cache hit for form ${ formId } `
235
+ } ) ;
236
+ configObj = JSON . parse ( jsonDataString ) ;
237
+ if ( ! sessionValidated ) {
238
+ // Validate cached form once per session
239
+ request . logger . debug ( {
240
+ ...LOGGER_DATA ,
241
+ message : `First access of form ${ formId } in yar session ${ request . yar . id } , validating cache`
242
+ } ) ;
243
+ const isValid = await this . validateCachedForm ( formId , configObj . hash , request ) ;
244
+ if ( ! isValid ) {
245
+ request . logger . info ( {
246
+ ...LOGGER_DATA ,
247
+ message : `Cache stale for form ${ formId } , fetching fresh version`
248
+ } ) ;
249
+ const freshConfig = await this . fetchAndCacheForm ( formId , request ) ;
250
+ if ( freshConfig ) {
251
+ configObj = freshConfig ;
252
+ }
253
+ } else {
254
+ request . logger . debug ( {
255
+ ...LOGGER_DATA ,
256
+ message : `Cache valid for form ${ formId } `
257
+ } ) ;
258
+ }
259
+ } else {
260
+ request . logger . debug ( {
261
+ ...LOGGER_DATA ,
262
+ message : `Form ${ formId } already validated in yar session ${ request . yar . id } `
263
+ } ) ;
264
+ }
265
+ } else {
266
+ // Cache miss - fetch from Pre-Award API
267
+ request . logger . info ( {
268
+ ...LOGGER_DATA ,
269
+ message : `Cache miss for form ${ formId } , fetching from Pre-Award API`
270
+ } ) ;
271
+ configObj = await this . fetchAndCacheForm ( formId , request ) ;
272
+ if ( ! configObj ) {
273
+ throw Boom . notFound ( `Form '${ formId } ' not found` ) ;
274
+ }
275
+ }
276
+ if ( ! sessionValidated ) {
277
+ // Mark form as validated in this session
278
+ request . logger . debug ( {
279
+ ...LOGGER_DATA ,
280
+ message : `Marking form ${ formId } as validated in yar session ${ request . yar . id } `
190
281
} ) ;
282
+ await this . formStorage . setex ( formSessionCacheKey , sessionTimeout / 1000 , true ) ;
191
283
}
192
- request . logger . error ( {
193
- ...LOGGER_DATA ,
194
- message : `[FORM-CACHE] Cannot find the form ${ formId } `
284
+ return new AdapterFormModel ( configObj . configuration , {
285
+ basePath : formId ,
286
+ hash : configObj . hash ,
287
+ previewMode : true ,
288
+ translationEn : translations . en ,
289
+ translationCy : translations . cy
195
290
} ) ;
196
- throw Boom . notFound ( "Cannot find the given form" ) ;
197
291
}
198
292
199
293
private getRedisClient ( ) : Redis | null {
0 commit comments