-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02-user-registration.ts
More file actions
161 lines (147 loc) · 4.22 KB
/
Copy path02-user-registration.ts
File metadata and controls
161 lines (147 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* User Registration Workflow
*
* Demonstrates:
* - Steps without compensation (validation)
* - Optional compensation
* - Global error registry for custom errors
*/
import * as restate from "@restatedev/restate-sdk";
import {
createSagaWorkflow,
createSagaStep,
StepResponse,
registerTerminalErrors,
} from "../src/index.js";
// Custom error classes
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
class DuplicateEmailError extends Error {
constructor(email: string) {
super(`Email ${email} is already registered`);
this.name = "DuplicateEmailError";
}
}
// Register errors that should trigger compensation without retry
registerTerminalErrors([ValidationError, DuplicateEmailError]);
// Step 1: Validate input (no compensation needed)
const validateInput = createSagaStep<
{ email: string; password: string },
{ valid: boolean },
null
>({
name: "ValidateInput",
run: async ({ input }) => {
if (!input.email.includes("@")) {
throw new ValidationError("Invalid email format");
}
if (input.password.length < 8) {
throw new ValidationError("Password must be at least 8 characters");
}
return new StepResponse({ valid: true }, null);
},
// No compensate - validation has no side effects
});
// Step 2: Create user account
const createUser = createSagaStep<
{ email: string; password: string },
{ userId: string },
{ userId: string }
>({
name: "CreateUser",
run: async ({ input }) => {
// Check for duplicates
if (input.email === "taken@example.com") {
throw new DuplicateEmailError(input.email);
}
const userId = `user_${Date.now()}`;
console.log(`Created user ${userId} with email ${input.email}`);
return new StepResponse({ userId }, { userId });
},
compensate: async (data) => {
if ("userId" in data) {
console.log(`Deleted user ${data.userId}`);
}
},
});
// Step 3: Send welcome email (optional compensation)
const sendWelcomeEmail = createSagaStep<
{ userId: string; email: string },
{ sent: boolean },
{ email: string }
>({
name: "SendWelcomeEmail",
run: async ({ input }) => {
console.log(`Sent welcome email to ${input.email}`);
return new StepResponse({ sent: true }, { email: input.email });
},
// Optional: log that we couldn't unsend the email
compensate: async (data) => {
if ("email" in data && typeof data.email === "string") {
console.log(`Note: Welcome email to ${data.email} was already sent`);
}
},
});
// Step 4: Create initial subscription
const createSubscription = createSagaStep<
{ userId: string; plan: string },
{ subscriptionId: string },
{ subscriptionId: string }
>({
name: "CreateSubscription",
run: async ({ input }) => {
if (input.plan === "INVALID") {
return StepResponse.permanentFailure("Invalid subscription plan", {
subscriptionId: "",
});
}
const subscriptionId = `sub_${Date.now()}`;
console.log(`Created ${input.plan} subscription for user ${input.userId}`);
return new StepResponse({ subscriptionId }, { subscriptionId });
},
compensate: async (data) => {
if ("subscriptionId" in data && data.subscriptionId) {
console.log(`Cancelled subscription ${data.subscriptionId}`);
}
},
});
// Registration workflow
export const registrationWorkflow = createSagaWorkflow(
"RegistrationWorkflow",
async (
saga,
input: {
email: string;
password: string;
plan: string;
}
) => {
// Validate first (no compensation needed)
await validateInput(saga, { email: input.email, password: input.password });
// Create user
const user = await createUser(saga, {
email: input.email,
password: input.password,
});
// Send welcome email
await sendWelcomeEmail(saga, {
userId: user.userId,
email: input.email,
});
// Create subscription
// If this fails, user creation is rolled back
const subscription = await createSubscription(saga, {
userId: user.userId,
plan: input.plan,
});
return {
userId: user.userId,
subscriptionId: subscription.subscriptionId,
};
}
);
restate.endpoint().bind(registrationWorkflow).listen(9080);