diff --git a/typescript/ticket-reservation/README.md b/typescript/ticket-reservation/README.md index d2f76d91..8c1fb3d8 100644 --- a/typescript/ticket-reservation/README.md +++ b/typescript/ticket-reservation/README.md @@ -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 git@github.com: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 ``` diff --git a/typescript/ticket-reservation/src/checkout.ts b/typescript/ticket-reservation/src/checkout.ts index d2091be9..151c606f 100644 --- a/typescript/ticket-reservation/src/checkout.ts +++ b/typescript/ticket-reservation/src/checkout.ts @@ -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 = { path: "CheckoutProcess", -}; -export const checkoutRouter = restate.router({ - checkout: doCheckout, -}); +}; \ No newline at end of file diff --git a/typescript/ticket-reservation/src/ticket_db.ts b/typescript/ticket-reservation/src/ticket_db.ts index 22eec96e..5b415715 100644 --- a/typescript/ticket-reservation/src/ticket_db.ts +++ b/typescript/ticket-reservation/src/ticket_db.ts @@ -17,47 +17,43 @@ enum TicketStatus { Sold, } -const doReserveTicket = async (ctx: restate.RpcContext) => { - const status = - (await ctx.get("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("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("status")) ?? TicketStatus.Available; +export const ticketDbRouter = restate.keyedRouter({ + reserve: async (ctx: restate.RpcContext) => { + const status = + (await ctx.get("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("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("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 = { path: "TicketDb", }; -export const ticketDbRouter = restate.keyedRouter({ - reserve: doReserveTicket, - unreserve: doUnreserveTicket, - markAsSold: doMarkAsSold, -}); diff --git a/typescript/ticket-reservation/src/user_session.ts b/typescript/ticket-reservation/src/user_session.ts index 58b59e1d..52ce601f 100644 --- a/typescript/ticket-reservation/src/user_session.ts +++ b/typescript/ticket-reservation/src/user_session.ts @@ -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("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("items")) ?? []; + if (reservation_response) { + // add ticket to user session items + const tickets = (await ctx.get("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("items")) ?? []; -const doCheckout = async (ctx: restate.RpcContext, userId: string) => { - const tickets = await ctx.get("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("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 = { path: "UserSession", }; -export const userSessionRouter = restate.keyedRouter({ - addTicket: doAddTicket, - expireTicket: doExpireTicket, - checkout: doCheckout, -});