@@ -10,10 +10,12 @@ import type {
10
10
import { EnvironmentVariablesService } from '../config' ;
11
11
import { IdempotencyRecord } from './IdempotencyRecord' ;
12
12
import { BasePersistenceLayerInterface } from './BasePersistenceLayerInterface' ;
13
- import { IdempotencyValidationError } from '../Exceptions' ;
13
+ import { IdempotencyItemAlreadyExistsError , IdempotencyValidationError } from '../Exceptions' ;
14
+ import { LRUCache } from './LRUCache' ;
14
15
15
16
abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
16
17
public idempotencyKeyPrefix : string ;
18
+ private cache ?: LRUCache < string , IdempotencyRecord > ;
17
19
private configured : boolean = false ;
18
20
// envVarsService is always initialized in the constructor
19
21
private envVarsService ! : EnvironmentVariablesService ;
@@ -25,7 +27,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
25
27
private useLocalCache : boolean = false ;
26
28
private validationKeyJmesPath ?: string ;
27
29
28
- public constructor ( ) {
30
+ public constructor ( ) {
29
31
this . envVarsService = new EnvironmentVariablesService ( ) ;
30
32
this . idempotencyKeyPrefix = this . getEnvVarsService ( ) . getFunctionName ( ) ;
31
33
}
@@ -55,7 +57,10 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
55
57
this . throwOnNoIdempotencyKey = idempotencyConfig ?. throwOnNoIdempotencyKey || false ;
56
58
this . eventKeyJmesPath = idempotencyConfig . eventKeyJmesPath ;
57
59
this . expiresAfterSeconds = idempotencyConfig . expiresAfterSeconds ; // 1 hour default
58
- // TODO: Add support for local cache
60
+ this . useLocalCache = idempotencyConfig . useLocalCache ;
61
+ if ( this . useLocalCache ) {
62
+ this . cache = new LRUCache ( { maxSize : idempotencyConfig . maxLocalCacheSize } ) ;
63
+ }
59
64
this . hashFunction = idempotencyConfig . hashFunction ;
60
65
}
61
66
@@ -64,13 +69,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
64
69
*
65
70
* @param data - the data payload that will be hashed to create the hash portion of the idempotency key
66
71
*/
67
- public async deleteRecord ( data : Record < string , unknown > ) : Promise < void > {
68
- const idempotencyRecord = new IdempotencyRecord ( {
72
+ public async deleteRecord ( data : Record < string , unknown > ) : Promise < void > {
73
+ const idempotencyRecord = new IdempotencyRecord ( {
69
74
idempotencyKey : this . getHashedIdempotencyKey ( data ) ,
70
75
status : IdempotencyRecordStatus . EXPIRED
71
76
} ) ;
72
-
77
+
73
78
await this . _deleteRecord ( idempotencyRecord ) ;
79
+
80
+ this . deleteFromCache ( idempotencyRecord . idempotencyKey ) ;
74
81
}
75
82
76
83
/**
@@ -81,7 +88,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
81
88
public async getRecord ( data : Record < string , unknown > ) : Promise < IdempotencyRecord > {
82
89
const idempotencyKey = this . getHashedIdempotencyKey ( data ) ;
83
90
91
+ const cachedRecord = this . getFromCache ( idempotencyKey ) ;
92
+ if ( cachedRecord ) {
93
+ this . validatePayload ( data , cachedRecord ) ;
94
+
95
+ return cachedRecord ;
96
+ }
97
+
84
98
const record = await this . _getRecord ( idempotencyKey ) ;
99
+ this . saveToCache ( record ) ;
85
100
this . validatePayload ( data , record ) ;
86
101
87
102
return record ;
@@ -97,7 +112,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
97
112
* @param data - the data payload that will be hashed to create the hash portion of the idempotency key
98
113
* @param remainingTimeInMillis - the remaining time left in the lambda execution context
99
114
*/
100
- public async saveInProgress ( data : Record < string , unknown > , remainingTimeInMillis ?: number ) : Promise < void > {
115
+ public async saveInProgress ( data : Record < string , unknown > , remainingTimeInMillis ?: number ) : Promise < void > {
101
116
const idempotencyRecord = new IdempotencyRecord ( {
102
117
idempotencyKey : this . getHashedIdempotencyKey ( data ) ,
103
118
status : IdempotencyRecordStatus . INPROGRESS ,
@@ -113,6 +128,10 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
113
128
) ;
114
129
}
115
130
131
+ if ( this . getFromCache ( idempotencyRecord . idempotencyKey ) ) {
132
+ throw new IdempotencyItemAlreadyExistsError ( ) ;
133
+ }
134
+
116
135
await this . _putRecord ( idempotencyRecord ) ;
117
136
}
118
137
@@ -123,7 +142,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
123
142
* @param data - the data payload that will be hashed to create the hash portion of the idempotency key
124
143
* @param result - the result of the successfully completed function
125
144
*/
126
- public async saveSuccess ( data : Record < string , unknown > , result : Record < string , unknown > ) : Promise < void > {
145
+ public async saveSuccess ( data : Record < string , unknown > , result : Record < string , unknown > ) : Promise < void > {
127
146
const idempotencyRecord = new IdempotencyRecord ( {
128
147
idempotencyKey : this . getHashedIdempotencyKey ( data ) ,
129
148
status : IdempotencyRecordStatus . COMPLETED ,
@@ -133,23 +152,33 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
133
152
} ) ;
134
153
135
154
await this . _updateRecord ( idempotencyRecord ) ;
155
+
156
+ this . saveToCache ( idempotencyRecord ) ;
136
157
}
137
158
138
159
protected abstract _deleteRecord ( record : IdempotencyRecord ) : Promise < void > ;
139
160
protected abstract _getRecord ( idempotencyKey : string ) : Promise < IdempotencyRecord > ;
140
161
protected abstract _putRecord ( record : IdempotencyRecord ) : Promise < void > ;
141
162
protected abstract _updateRecord ( record : IdempotencyRecord ) : Promise < void > ;
142
163
164
+ private deleteFromCache ( idempotencyKey : string ) : void {
165
+ if ( ! this . useLocalCache ) return ;
166
+ // Delete from local cache if it exists
167
+ if ( this . cache ?. has ( idempotencyKey ) ) {
168
+ this . cache ?. remove ( idempotencyKey ) ;
169
+ }
170
+ }
171
+
143
172
/**
144
173
* Generates a hash of the data and returns the digest of that hash
145
174
*
146
175
* @param data the data payload that will generate the hash
147
176
* @returns the digest of the generated hash
148
177
*/
149
- private generateHash ( data : string ) : string {
178
+ private generateHash ( data : string ) : string {
150
179
const hash : Hash = createHash ( this . hashFunction ) ;
151
180
hash . update ( data ) ;
152
-
181
+
153
182
return hash . digest ( 'base64' ) ;
154
183
}
155
184
@@ -168,10 +197,21 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
168
197
*/
169
198
private getExpiryTimestamp ( ) : number {
170
199
const currentTime : number = Date . now ( ) / 1000 ;
171
-
200
+
172
201
return currentTime + this . expiresAfterSeconds ;
173
202
}
174
203
204
+ private getFromCache ( idempotencyKey : string ) : IdempotencyRecord | undefined {
205
+ if ( ! this . useLocalCache ) return undefined ;
206
+ const cachedRecord = this . cache ?. get ( idempotencyKey ) ;
207
+ if ( cachedRecord ) {
208
+ // if record is not expired, return it
209
+ if ( ! cachedRecord . isExpired ( ) ) return cachedRecord ;
210
+ // if record is expired, delete it from cache
211
+ this . deleteFromCache ( idempotencyKey ) ;
212
+ }
213
+ }
214
+
175
215
/**
176
216
* Generates the idempotency key used to identify records in the persistence store.
177
217
*
@@ -182,14 +222,14 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
182
222
if ( this . eventKeyJmesPath ) {
183
223
data = search ( data , this . eventKeyJmesPath ) ;
184
224
}
185
-
225
+
186
226
if ( BasePersistenceLayer . isMissingIdempotencyKey ( data ) ) {
187
227
if ( this . throwOnNoIdempotencyKey ) {
188
228
throw new Error ( 'No data found to create a hashed idempotency_key' ) ;
189
229
}
190
230
console . warn ( `No value found for idempotency_key. jmespath: ${ this . eventKeyJmesPath } ` ) ;
191
231
}
192
-
232
+
193
233
return `${ this . idempotencyKeyPrefix } #${ this . generateHash ( JSON . stringify ( data ) ) } ` ;
194
234
}
195
235
@@ -204,7 +244,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
204
244
// Therefore, the assertion is safe.
205
245
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
206
246
data = search ( data , this . validationKeyJmesPath ! ) ;
207
-
247
+
208
248
return this . generateHash ( JSON . stringify ( data ) ) ;
209
249
}
210
250
@@ -223,6 +263,20 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
223
263
return ! data ;
224
264
}
225
265
266
+ /**
267
+ * Save record to local cache except for when status is `INPROGRESS`.
268
+ *
269
+ * We can't cache `INPROGRESS` records because we have no way to reflect updates
270
+ * that might happen to the record outside of the execution context of the function.
271
+ *
272
+ * @param record - record to save
273
+ */
274
+ private saveToCache ( record : IdempotencyRecord ) : void {
275
+ if ( ! this . useLocalCache ) return ;
276
+ if ( record . getStatus ( ) === IdempotencyRecordStatus . INPROGRESS ) return ;
277
+ this . cache ?. add ( record . idempotencyKey , record ) ;
278
+ }
279
+
226
280
private validatePayload ( data : Record < string , unknown > , record : IdempotencyRecord ) : void {
227
281
if ( this . payloadValidationEnabled ) {
228
282
const hashedPayload : string = this . getHashedPayload ( data ) ;
0 commit comments