Skip to content

Commit

Permalink
Make Handler API more concise for ticket reservation app
Browse files Browse the repository at this point in the history
  • Loading branch information
gvdongen committed Feb 1, 2024
1 parent 1cd4f30 commit 7d4e879
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 149 deletions.
19 changes: 2 additions & 17 deletions typescript/ticket-reservation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,26 +81,11 @@ Here is an incomplete list of simplifications to the application that are possib
- Latest stable version of [NodeJS](https://nodejs.org/en/) >= v18.17.1 and [npm CLI](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) >= 9.6.7 installed.
- [Docker Engine](https://docs.docker.com/engine/install/) to launch the Restate runtime (not needed for the app implementation itself).
### Setup
Clone this GitHub repository and navigate to the `ticket-reservation` folder:
```shell
git clone [email protected]:restatedev/examples.git
cd ticket-reservation
```
Next, install the dependencies:
### Run the example
Install the dependencies, build the app and run it:
```shell
npm install
```
### Run the service
Once you are done with the implementation, build/run the example app with:
```shell
npm run build
npm run app
```
Expand Down
65 changes: 32 additions & 33 deletions typescript/ticket-reservation/src/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,39 @@ import { v4 as uuid } from "uuid";
import { StripeClient } from "./auxiliary/stripe_client";
import { EmailClient } from "./auxiliary/email_client";

const doCheckout = async (
ctx: restate.RpcContext,
request: { userId: string; tickets: string[] }
) => {
// We are a uniform shop where everything costs 40 USD
const totalPrice = request.tickets.length * 40;

// Generate idempotency key for the stripe client
const idempotencyKey = await ctx.sideEffect(async () => uuid());
const stripe = StripeClient.get();

const doPayment = async () => stripe.call(idempotencyKey, totalPrice);
const success = await ctx.sideEffect(doPayment);

const email = EmailClient.get();

if (success) {
console.info("Payment successful. Notifying user about shipment.");
await ctx.sideEffect(async () =>
email.notifyUserOfPaymentSuccess(request.userId)
);
} else {
console.info("Payment failure. Notifying user about it.");
await ctx.sideEffect(async () =>
email.notifyUserOfPaymentFailure(request.userId)
);
export const checkoutRouter = restate.router({
checkout: async (
ctx: restate.RpcContext,
request: { userId: string; tickets: string[] }
) => {
// We are a uniform shop where everything costs 40 USD
const totalPrice = request.tickets.length * 40;

// Generate idempotency key for the stripe client
const idempotencyKey = await ctx.sideEffect(async () => uuid());
const stripe = StripeClient.get();

const doPayment = async () => stripe.call(idempotencyKey, totalPrice);
const success = await ctx.sideEffect(doPayment);

const email = EmailClient.get();

if (success) {
console.info("Payment successful. Notifying user about shipment.");
await ctx.sideEffect(async () =>
email.notifyUserOfPaymentSuccess(request.userId)
);
} else {
console.info("Payment failure. Notifying user about it.");
await ctx.sideEffect(async () =>
email.notifyUserOfPaymentFailure(request.userId)
);
}

return success;
}

return success;
};
});

export const checkoutApi: restate.ServiceApi<typeof checkoutRouter> = {
path: "CheckoutProcess",
};
export const checkoutRouter = restate.router({
checkout: doCheckout,
});
};
74 changes: 35 additions & 39 deletions typescript/ticket-reservation/src/ticket_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,47 +17,43 @@ enum TicketStatus {
Sold,
}

const doReserveTicket = async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Available) {
ctx.set("status", TicketStatus.Reserved);
return true;
} else {
return false;
}
};

const doUnreserveTicket = async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Sold) {
return false;
} else {
ctx.clear("status");
return true;
}
};

const doMarkAsSold = async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;
export const ticketDbRouter = restate.keyedRouter({
reserve: async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Available) {
ctx.set("status", TicketStatus.Reserved);
return true;
} else {
return false;
}
},
unreserve: async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Sold) {
return false;
} else {
ctx.clear("status");
return true;
}
},
markAsSold: async (ctx: restate.RpcContext) => {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Reserved) {
ctx.set("status", TicketStatus.Sold);
return true;
} else {
return false;
}
},
});

if (status === TicketStatus.Reserved) {
ctx.set("status", TicketStatus.Sold);
return true;
} else {
return false;
}
};

export const ticketDbApi: restate.ServiceApi<typeof ticketDbRouter> = {
path: "TicketDb",
};
export const ticketDbRouter = restate.keyedRouter({
reserve: doReserveTicket,
unreserve: doUnreserveTicket,
markAsSold: doMarkAsSold,
});
115 changes: 55 additions & 60 deletions typescript/ticket-reservation/src/user_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,76 +13,71 @@ import * as restate from "@restatedev/restate-sdk";
import { ticketDbApi } from "./ticket_db";
import { checkoutApi } from "./checkout";

const doAddTicket = async (
ctx: restate.RpcContext,
userId: string,
ticketId: string
) => {
// try to reserve ticket
const reservation_response = await ctx.rpc(ticketDbApi).reserve(ticketId);

if (reservation_response) {
// add ticket to user session items
const tickets = (await ctx.get<string[]>("items")) ?? [];
tickets.push(ticketId);
ctx.set("items", tickets);

// Schedule expiry timer
ctx
.sendDelayed(userSessionApi, 15 * 60 * 1000)
.expireTicket(userId, ticketId);
}

return reservation_response;
};
export const userSessionRouter = restate.keyedRouter({
addTicket: async (
ctx: restate.RpcContext,
userId: string,
ticketId: string
) => {
// try to reserve ticket
const reservation_response = await ctx.rpc(ticketDbApi).reserve(ticketId);

const doExpireTicket = async (
ctx: restate.RpcContext,
userId: string,
ticketId: string
) => {
ctx.send(ticketDbApi).unreserve(ticketId);
const tickets = (await ctx.get<string[]>("items")) ?? [];
if (reservation_response) {
// add ticket to user session items
const tickets = (await ctx.get<string[]>("items")) ?? [];
tickets.push(ticketId);
ctx.set("items", tickets);

const index = tickets.findIndex((id) => id === ticketId);
// Schedule expiry timer
ctx
.sendDelayed(userSessionApi, 15 * 60 * 1000)
.expireTicket(userId, ticketId);
}

// try removing ticket
if (index != -1) {
tickets.splice(index, 1);
ctx.set("items", tickets);
// unreserve if ticket was reserved before
return reservation_response;
},
expireTicket: async (
ctx: restate.RpcContext,
userId: string,
ticketId: string
) => {
ctx.send(ticketDbApi).unreserve(ticketId);
}
};
const tickets = (await ctx.get<string[]>("items")) ?? [];

const doCheckout = async (ctx: restate.RpcContext, userId: string) => {
const tickets = await ctx.get<string[]>("items");
const index = tickets.findIndex((id) => id === ticketId);

if (tickets && tickets.length > 0) {
const checkout_success = await ctx
.rpc(checkoutApi)
.checkout({ userId: userId, tickets: tickets! });
// try removing ticket
if (index != -1) {
tickets.splice(index, 1);
ctx.set("items", tickets);
// unreserve if ticket was reserved before
ctx.send(ticketDbApi).unreserve(ticketId);
}
},
checkout: async (ctx: restate.RpcContext, userId: string) => {
const tickets = await ctx.get<string[]>("items");

if (tickets && tickets.length > 0) {
const checkout_success = await ctx
.rpc(checkoutApi)
.checkout({ userId: userId, tickets: tickets! });

if (checkout_success) {
// mark items as sold if checkout was successful
for (const ticket_id of tickets) {
ctx.send(ticketDbApi).markAsSold(ticket_id);
if (checkout_success) {
// mark items as sold if checkout was successful
for (const ticket_id of tickets) {
ctx.send(ticketDbApi).markAsSold(ticket_id);
}
ctx.clear("items");
}
ctx.clear("items");
}

return checkout_success;
} else {
// no tickets reserved
return false;
}
};
return checkout_success;
} else {
// no tickets reserved
return false;
}
},
});

export const userSessionApi: restate.ServiceApi<typeof userSessionRouter> = {
path: "UserSession",
};
export const userSessionRouter = restate.keyedRouter({
addTicket: doAddTicket,
expireTicket: doExpireTicket,
checkout: doCheckout,
});

0 comments on commit 7d4e879

Please sign in to comment.