Skip to content

Commit bba1c01

Browse files
authored
fix(idempotency): types, docs, and makeIdempotent function wrapper (#1579)
* fix: misc fixes from feedback * chore: add makeIdempotent wrapper to jest config (it was excluded) * chore: made error cause mandatory * chore: renamed wrapper function, fixed arguments handling, fixed types
1 parent 2f0ecb9 commit bba1c01

20 files changed

+792
-450
lines changed

Diff for: packages/idempotency/README.md

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Powertools for AWS Lambda (TypeScript) - Idempotency Utility <!-- omit in toc -->
22

33

4-
| ⚠️ **WARNING: Do not use this utility in production just yet!** ⚠️ |
5-
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4+
| ⚠️ **WARNING: Do not use this utility in production just yet!** ⚠️ |
5+
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
| **This utility is currently released as beta developer preview** and is intended strictly for feedback and testing purposes **and not for production workloads**.. The version and all future versions tagged with the `-beta` suffix should be treated as not stable. Up until before the [General Availability release](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/10) we might introduce significant breaking changes and improvements in response to customers feedback. | _ |
77

88

@@ -29,7 +29,7 @@ You can use the package in both TypeScript and JavaScript code bases.
2929
## Intro
3030

3131
This package provides a utility to implement idempotency in your Lambda functions.
32-
You can either use it to wrapp a function, or as Middy middleware to make your AWS Lambda handler idempotent.
32+
You can either use it to wrap a function, or as Middy middleware to make your AWS Lambda handler idempotent.
3333

3434
The current implementation provides a persistence layer for Amazon DynamoDB, which offers a variety of configuration options. You can also bring your own persistence layer by extending the `BasePersistenceLayer` class.
3535

@@ -59,7 +59,7 @@ The function wrapper takes a reference to the function to be made idempotent as
5959

6060
```ts
6161
import { makeFunctionIdempotent } from '@aws-lambda-powertools/idempotency';
62-
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/persistence';
62+
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
6363
import type { Context, SQSEvent, SQSRecord } from 'aws-lambda';
6464

6565
const persistenceStore = new DynamoDBPersistenceLayer({
@@ -75,7 +75,7 @@ export const handler = async (
7575
_context: Context
7676
): Promise<void> => {
7777
for (const record of event.Records) {
78-
await makeFunctionIdempotent(proccessingFunction, {
78+
await makeFunctionIdempotent(processingFunction, {
7979
dataKeywordArgument: 'transactionId',
8080
persistenceStore,
8181
});
@@ -96,7 +96,7 @@ By default, the Idempotency utility will use the full event payload to create an
9696
```ts
9797
import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
9898
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
99-
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/persistence';
99+
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
100100
import middy from '@middy/core';
101101
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';
102102

@@ -111,10 +111,6 @@ const config = new IdempotencyConfig({
111111
eventKeyJmesPath: 'headers.idempotency-key',
112112
});
113113

114-
const processingFunction = async (payload: SQSRecord): Promise<void> => {
115-
// your code goes here here
116-
};
117-
118114
export const handler = middy(
119115
async (event: APIGatewayProxyEvent, _context: Context): Promise<void> => {
120116
// your code goes here here

Diff for: packages/idempotency/jest.config.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ module.exports = {
1414
roots: ['<rootDir>/src', '<rootDir>/tests'],
1515
testPathIgnorePatterns: ['/node_modules/'],
1616
testEnvironment: 'node',
17-
coveragePathIgnorePatterns: [
18-
'/node_modules/',
19-
'/types/',
20-
'src/makeFunctionIdempotent.ts', // TODO: remove this once makeFunctionIdempotent is implemented
21-
],
17+
coveragePathIgnorePatterns: ['/node_modules/', '/types/'],
2218
coverageThreshold: {
2319
global: {
2420
statements: 100,

Diff for: packages/idempotency/src/IdempotencyHandler.ts

+66-37
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { AnyFunctionWithRecord, IdempotencyHandlerOptions } from './types';
1+
import type { JSONValue } from '@aws-lambda-powertools/commons';
2+
import type { AnyFunction, IdempotencyHandlerOptions } from './types';
23
import { IdempotencyRecordStatus } from './types';
34
import {
45
IdempotencyAlreadyInProgressError,
@@ -13,31 +14,57 @@ import { search } from 'jmespath';
1314

1415
/**
1516
* @internal
17+
*
18+
* Class that handles the idempotency lifecycle.
19+
*
20+
* This class is used under the hood by the Idempotency utility
21+
* and provides several methods that are called at different stages
22+
* to orchestrate the idempotency logic.
1623
*/
17-
export class IdempotencyHandler<U> {
18-
private readonly fullFunctionPayload: Record<string, unknown>;
19-
private readonly functionPayloadToBeHashed: Record<string, unknown>;
20-
private readonly functionToMakeIdempotent: AnyFunctionWithRecord<U>;
21-
private readonly idempotencyConfig: IdempotencyConfig;
22-
private readonly persistenceStore: BasePersistenceLayer;
24+
export class IdempotencyHandler<Func extends AnyFunction> {
25+
/**
26+
* The arguments passed to the function.
27+
*
28+
* For example, if the function is `foo(a, b)`, then `functionArguments` will be `[a, b]`.
29+
* We need to keep track of the arguments so that we can pass them to the function when we call it.
30+
*/
31+
readonly #functionArguments: unknown[];
32+
/**
33+
* The payload to be hashed.
34+
*
35+
* This is the argument that is used for the idempotency.
36+
*/
37+
readonly #functionPayloadToBeHashed: JSONValue;
38+
/**
39+
* Reference to the function to be made idempotent.
40+
*/
41+
readonly #functionToMakeIdempotent: AnyFunction;
42+
/**
43+
* Idempotency configuration options.
44+
*/
45+
readonly #idempotencyConfig: IdempotencyConfig;
46+
/**
47+
* Persistence layer used to store the idempotency records.
48+
*/
49+
readonly #persistenceStore: BasePersistenceLayer;
2350

24-
public constructor(options: IdempotencyHandlerOptions<U>) {
51+
public constructor(options: IdempotencyHandlerOptions) {
2552
const {
2653
functionToMakeIdempotent,
2754
functionPayloadToBeHashed,
2855
idempotencyConfig,
29-
fullFunctionPayload,
56+
functionArguments,
3057
persistenceStore,
3158
} = options;
32-
this.functionToMakeIdempotent = functionToMakeIdempotent;
33-
this.functionPayloadToBeHashed = functionPayloadToBeHashed;
34-
this.idempotencyConfig = idempotencyConfig;
35-
this.fullFunctionPayload = fullFunctionPayload;
59+
this.#functionToMakeIdempotent = functionToMakeIdempotent;
60+
this.#functionPayloadToBeHashed = functionPayloadToBeHashed;
61+
this.#idempotencyConfig = idempotencyConfig;
62+
this.#functionArguments = functionArguments;
3663

37-
this.persistenceStore = persistenceStore;
64+
this.#persistenceStore = persistenceStore;
3865

39-
this.persistenceStore.configure({
40-
config: this.idempotencyConfig,
66+
this.#persistenceStore.configure({
67+
config: this.#idempotencyConfig,
4168
});
4269
}
4370

@@ -69,14 +96,14 @@ export class IdempotencyHandler<U> {
6996
return idempotencyRecord.getResponse();
7097
}
7198

72-
public async getFunctionResult(): Promise<U> {
73-
let result: U;
99+
public async getFunctionResult(): Promise<ReturnType<Func>> {
100+
let result;
74101
try {
75-
result = await this.functionToMakeIdempotent(this.fullFunctionPayload);
102+
result = await this.#functionToMakeIdempotent(...this.#functionArguments);
76103
} catch (e) {
77104
try {
78-
await this.persistenceStore.deleteRecord(
79-
this.functionPayloadToBeHashed
105+
await this.#persistenceStore.deleteRecord(
106+
this.#functionPayloadToBeHashed
80107
);
81108
} catch (e) {
82109
throw new IdempotencyPersistenceLayerError(
@@ -87,9 +114,9 @@ export class IdempotencyHandler<U> {
87114
throw e;
88115
}
89116
try {
90-
await this.persistenceStore.saveSuccess(
91-
this.functionPayloadToBeHashed,
92-
result as Record<string, unknown>
117+
await this.#persistenceStore.saveSuccess(
118+
this.#functionPayloadToBeHashed,
119+
result
93120
);
94121
} catch (e) {
95122
throw new IdempotencyPersistenceLayerError(
@@ -108,7 +135,7 @@ export class IdempotencyHandler<U> {
108135
* window, we might get an `IdempotencyInconsistentStateError`. In such
109136
* cases we can safely retry the handling a few times.
110137
*/
111-
public async handle(): Promise<U> {
138+
public async handle(): Promise<ReturnType<Func>> {
112139
let e;
113140
for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) {
114141
try {
@@ -129,34 +156,36 @@ export class IdempotencyHandler<U> {
129156
throw e;
130157
}
131158

132-
public async processIdempotency(): Promise<U> {
159+
public async processIdempotency(): Promise<ReturnType<Func>> {
133160
// early return if we should skip idempotency completely
134161
if (
135162
IdempotencyHandler.shouldSkipIdempotency(
136-
this.idempotencyConfig.eventKeyJmesPath,
137-
this.idempotencyConfig.throwOnNoIdempotencyKey,
138-
this.fullFunctionPayload
163+
this.#idempotencyConfig.eventKeyJmesPath,
164+
this.#idempotencyConfig.throwOnNoIdempotencyKey,
165+
this.#functionPayloadToBeHashed
139166
)
140167
) {
141-
return await this.functionToMakeIdempotent(this.fullFunctionPayload);
168+
return await this.#functionToMakeIdempotent(...this.#functionArguments);
142169
}
143170

144171
try {
145-
await this.persistenceStore.saveInProgress(
146-
this.functionPayloadToBeHashed,
147-
this.idempotencyConfig.lambdaContext?.getRemainingTimeInMillis()
172+
await this.#persistenceStore.saveInProgress(
173+
this.#functionPayloadToBeHashed,
174+
this.#idempotencyConfig.lambdaContext?.getRemainingTimeInMillis()
148175
);
149176
} catch (e) {
150177
if (e instanceof IdempotencyItemAlreadyExistsError) {
151178
const idempotencyRecord: IdempotencyRecord =
152-
await this.persistenceStore.getRecord(this.functionPayloadToBeHashed);
179+
await this.#persistenceStore.getRecord(
180+
this.#functionPayloadToBeHashed
181+
);
153182

154183
return IdempotencyHandler.determineResultFromIdempotencyRecord(
155184
idempotencyRecord
156-
) as U;
185+
) as ReturnType<Func>;
157186
} else {
158187
throw new IdempotencyPersistenceLayerError(
159-
'Failed to save record in progress',
188+
'Failed to save in progress record to idempotency store',
160189
e as Error
161190
);
162191
}
@@ -177,7 +206,7 @@ export class IdempotencyHandler<U> {
177206
public static shouldSkipIdempotency(
178207
eventKeyJmesPath: string,
179208
throwOnNoIdempotencyKey: boolean,
180-
fullFunctionPayload: Record<string, unknown>
209+
fullFunctionPayload: JSONValue
181210
): boolean {
182211
return (eventKeyJmesPath &&
183212
!throwOnNoIdempotencyKey &&

Diff for: packages/idempotency/src/errors.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,8 @@ class IdempotencyInconsistentStateError extends Error {}
3434
class IdempotencyPersistenceLayerError extends Error {
3535
public readonly cause: Error | undefined;
3636

37-
public constructor(message: string, cause?: Error) {
38-
const errorMessage = cause
39-
? `${message}. This error was caused by: ${cause.message}.`
40-
: message;
37+
public constructor(message: string, cause: Error) {
38+
const errorMessage = `${message}. This error was caused by: ${cause.message}.`;
4139
super(errorMessage);
4240
this.cause = cause;
4341
}

Diff for: packages/idempotency/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './errors';
22
export * from './IdempotencyConfig';
3-
export * from './makeFunctionIdempotent';
3+
export * from './makeIdempotent';

Diff for: packages/idempotency/src/makeFunctionIdempotent.ts

-87
This file was deleted.

0 commit comments

Comments
 (0)