Skip to content

Commit 1fd8959

Browse files
authored
test: add basic step serde example (#371)
*Issue #, if available:* n/a *Description of changes:* Add a basic step example explaining why and how to use custom SerDe via `createClassSerdes`. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Signed-off-by: Michael Gasch <15986659+embano1@users.noreply.github.com>
1 parent afe2020 commit 1fd8959

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
OperationStatus,
3+
OperationType,
4+
} from "@aws/durable-execution-sdk-js-testing";
5+
import { createTests } from "../../../utils/test-helper";
6+
import { handler } from "./serde-basic";
7+
8+
createTests({
9+
name: "serde-basic test",
10+
functionName: "serde-basic",
11+
handler,
12+
tests: (runner) => {
13+
it("should preserve User class methods across replay with createClassSerdes", async () => {
14+
const execution = await runner.run({
15+
payload: {
16+
firstName: "John",
17+
lastName: "Doe",
18+
email: "john.doe@example.com",
19+
},
20+
});
21+
22+
// Verify at least 2 invocations (initial + replay after wait)
23+
expect(execution.getInvocations().length).toBe(2);
24+
25+
// Verify we have 3 operations: create-user step, wait, greet-user step
26+
expect(execution.getOperations().length).toBe(3);
27+
28+
// Verify first step (create-user) succeeded
29+
const createUserStep = runner.getOperation("create-user");
30+
expect(createUserStep).toBeDefined();
31+
expect(createUserStep.getType()).toBe(OperationType.STEP);
32+
expect(createUserStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
33+
34+
const stepResult = createUserStep.getStepDetails()?.result as any;
35+
expect(stepResult.firstName).toBe("John");
36+
expect(stepResult.lastName).toBe("Doe");
37+
expect(stepResult.email).toBe("john.doe@example.com");
38+
39+
// Verify wait operation exists
40+
const waitOp = runner.getOperationByIndex(1);
41+
expect(waitOp.getType()).toBe(OperationType.WAIT);
42+
expect(waitOp.getStatus()).toBe(OperationStatus.SUCCEEDED);
43+
44+
// Verify second step (greet-user) succeeded
45+
const greetUserStep = runner.getOperation("greet-user");
46+
expect(greetUserStep).toBeDefined();
47+
expect(greetUserStep.getType()).toBe(OperationType.STEP);
48+
expect(greetUserStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
49+
expect(greetUserStep.getStepDetails()?.error).toBeUndefined();
50+
expect(greetUserStep.getStepDetails()?.result).toBe(
51+
"Hello, I'm John Doe. My email is john.doe@example.com",
52+
);
53+
54+
// Verify final result
55+
const result = execution.getResult() as any;
56+
expect(result.user.firstName).toBe("John");
57+
expect(result.user.lastName).toBe("Doe");
58+
expect(result.user.email).toBe("john.doe@example.com");
59+
expect(result.greeting).toBe(
60+
"Hello, I'm John Doe. My email is john.doe@example.com",
61+
);
62+
});
63+
64+
it("should work with different user data", async () => {
65+
const execution = await runner.run({
66+
payload: {
67+
firstName: "Alice",
68+
lastName: "Smith",
69+
email: "alice@example.com",
70+
},
71+
});
72+
73+
// Verify the greeting was generated correctly using class methods
74+
const result = execution.getResult() as any;
75+
expect(result.greeting).toBe(
76+
"Hello, I'm Alice Smith. My email is alice@example.com",
77+
);
78+
79+
// Verify all operations succeeded
80+
const createUserStep = runner.getOperation("create-user");
81+
const greetUserStep = runner.getOperation("greet-user");
82+
expect(createUserStep.getStepDetails()?.error).toBeUndefined();
83+
expect(greetUserStep.getStepDetails()?.error).toBeUndefined();
84+
85+
// This proves createClassSerdes successfully preserved the User class methods
86+
// during deserialization after the wait/replay
87+
});
88+
},
89+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
DurableContext,
3+
withDurableExecution,
4+
createClassSerdes,
5+
} from "@aws/durable-execution-sdk-js";
6+
import { ExampleConfig } from "../../../types";
7+
8+
export const config: ExampleConfig = {
9+
name: "Basic Serdes with createClassSerdes",
10+
description:
11+
"Demonstrates using createClassSerdes to preserve class methods across replay. " +
12+
"Shows how default serdes would lose methods, but createClassSerdes maintains them.",
13+
};
14+
15+
/**
16+
* User class demonstrating the need for createClassSerdes.
17+
*
18+
* IMPORTANT: For createClassSerdes to work, the class must have a
19+
* no-argument constructor (or all parameters must be optional).
20+
* This is because createClassSerdes calls `new User()` during deserialization,
21+
* then uses Object.assign to copy the properties.
22+
*/
23+
class User {
24+
firstName: string = "";
25+
lastName: string = "";
26+
email: string = "";
27+
28+
constructor(firstName?: string, lastName?: string, email?: string) {
29+
if (firstName) this.firstName = firstName;
30+
if (lastName) this.lastName = lastName;
31+
if (email) this.email = email;
32+
}
33+
34+
/**
35+
* Method that would be lost with default serdes, but preserved with createClassSerdes
36+
*/
37+
getFullName(): string {
38+
return `${this.firstName} ${this.lastName}`;
39+
}
40+
41+
/**
42+
* Method that would be lost with default serdes, but preserved with createClassSerdes
43+
*/
44+
greet(): string {
45+
return `Hello, I'm ${this.getFullName()}. My email is ${this.email}`;
46+
}
47+
}
48+
49+
export const handler = withDurableExecution(
50+
async (
51+
event: { firstName: string; lastName: string; email: string },
52+
context: DurableContext,
53+
) => {
54+
// Create custom serdes for User class
55+
const userSerdes = createClassSerdes(User);
56+
57+
// Step 1: Create user with createClassSerdes to preserve methods
58+
// Without this custom serdes, methods would be lost after replay
59+
const user = await context.step(
60+
"create-user",
61+
async () => {
62+
const newUser = new User(event.firstName, event.lastName, event.email);
63+
context.logger.info("Created user:", {
64+
fullName: newUser.getFullName(),
65+
});
66+
return newUser;
67+
},
68+
{ serdes: userSerdes },
69+
);
70+
71+
// Wait for 1 second - this forces a replay when resumed
72+
// On replay, the step above will deserialize using createClassSerdes
73+
await context.wait({ seconds: 1 });
74+
75+
// Step 2: Use user methods - works because createClassSerdes preserved them
76+
// If we had used default serdes, user.greet() would throw "is not a function"
77+
const greeting = await context.step("greet-user", async () => {
78+
return user.greet();
79+
});
80+
81+
return {
82+
user: {
83+
firstName: user.firstName,
84+
lastName: user.lastName,
85+
email: user.email,
86+
},
87+
greeting,
88+
};
89+
},
90+
);

packages/aws-durable-execution-sdk-js-examples/template.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,31 @@ Resources:
15001500
DURABLE_EXAMPLES_VERBOSE: "true"
15011501
Metadata:
15021502
SkipBuild: "True"
1503+
SerdeBasic:
1504+
Type: AWS::Serverless::Function
1505+
Properties:
1506+
FunctionName: SerdeBasic-TypeScript
1507+
CodeUri: ./dist
1508+
Handler: serde-basic.handler
1509+
Runtime: nodejs22.x
1510+
Architectures:
1511+
- x86_64
1512+
MemorySize: 128
1513+
Timeout: 60
1514+
Role:
1515+
Fn::GetAtt:
1516+
- DurableFunctionRole
1517+
- Arn
1518+
DurableConfig:
1519+
ExecutionTimeout: 3600
1520+
RetentionPeriodInDays: 7
1521+
Environment:
1522+
Variables:
1523+
AWS_ENDPOINT_URL_LAMBDA: http://host.docker.internal:5000
1524+
DURABLE_VERBOSE_MODE: "false"
1525+
DURABLE_EXAMPLES_VERBOSE: "true"
1526+
Metadata:
1527+
SkipBuild: "True"
15031528
SimpleExecution:
15041529
Type: AWS::Serverless::Function
15051530
Properties:

0 commit comments

Comments
 (0)