0:00 All right, so the next step will be to reserve the 0:02 stock with our inventory 0:03 service. So let's go ahead and now await context.step, 0:08 and we'll go ahead and label this step reserve stock. 0:12 Again, this can be anything we want, which will identify the step inside of 0:16 the database, 0:18 so that when we rerun this, it'll return the original stored result. 0:23 So for the actual invocation, we're going to go ahead and again 0:26 destructure that idempotency key, 0:30 and then return this inventoryService.reserve, 0:34 which takes in the order items. So we can pull this directly 0:38 off of the order.items, and then the idempotency 0:42 key to make sure that this is not duplicated more than once. 0:46 And just like payment, we'll also add our compensating 0:50 step. So we'll add our options object and set compensate 0:55 to a new callback where we destructure that reservation 0:59 ID now. That's what gets returned from reserve. 1:03 Remember, here we return back the reservation ID 1:06 that we stored 1:07 or already existed in the database from this 1:11 idempotency key 1:13 after we update our product stock. 1:17 And so I'm going to then 1:20 use this to call 1:22 inventoryService.release and pass through that 1:25 reservation ID now. And we don't have to have an 1:29 idempotency key here. Now at this point in the workflow, 1:32 we've charged 1:32 the user, we've reserved the stock. 1:35 We have ways to undo this as well if we fail at any point. 1:39 And this is key. If, for example, reserve stock failed here 1:43 inside of the inventory service and we exhausted all the retries, 1:47 or we reached our timeout of thirty days, for example, we 1:51 would fail the workflow. And as we saw with compensate, that is 1:55 going to undo these steps in reverse order. 1:58 So we will call reverse stock compensate to release, and then 2:02 we will call charge payment compensate. 2:04 And we'll see this later on when we test this out with some r- 2:08 failures introduced. I'll also go ahead and update our 2:11 context to now set status as awaiting 2:16 delivery. Because at this point in our system, 2:19 we have charged the user, 2:20 and we've updated the stock in our system. 2:23 This is what is going to then trigger delivery in our system. 2:28 And so in our application, we will dispatch this event to 2:31 deliver the product, for example. 2:34 And then we're going to implement a web hook controller that will 2:38 essentially be notification for when the product 2:41 does get delivered. And so what we have at this point 2:44 is essentially a 2:45 period of time that we now need to wait inside of the workflow. 2:50 This delivery, as we know, could take 2:53 several weeks to actually deliver and get to the user. 2:57 So we essentially want to place this workflow in a pause 3:00 state and only resume it once we receive that 3:04 notification from the shipper that it has been delivered. 3:07 And that's what this workflow offers us in the form of 3:11 waiting. We can actually tell NestJS workflow to 3:15 essentially pause this instance and say, we want to 3:19 stop running this workflow, wait a period of time, and then we 3:23 can pick it back up because all of this information about 3:27 the workflow has already been stored inside of the database in 3:30 that journal. And this is really powerful because we can actually 3:34 stop the execution of this workflow, wait any period of 3:38 time or any event, and resume it from the beginning. 3:43 Since these workflow steps have already succeeded and have been stored in the 3:47 journal, they will not be replayed. 3:49 Instead, the returned result from the database will 3:52 be used here, and then 3:53 we'll be back up to this point and can implement new functionality. 3:58 So this is an entirely different mental model, and I know it can take a bit for it 4:01 to click. So let's go ahead and actually implement this waiting 4:05 functionality, and we'll see how it all work in the end 4:08 through some 4:09 examples. 4:10 All right, so how do we actually do this, implement this waiting 4:14 functionality, where now we've charged the user, 4:17 and we've updated the 4:18 stock, and we've sent out the order to be 4:22 shipped. We want to wait and resume this workflow instance 4:26 only when we have a 4:29 update from the carrier so that the order was delivered, 4:33 returned, or it timed out. Well, you can implement a 4:37 wait inside of your workflow using the context object. 4:42 So here what we're going to do is create a new const outcome 4:45 and set it equal 4:46 to await context., 4:49 and it has a method called 4:51 waitForAny. This is the same for 4:55 waitForSignal, but waitForAny allows us to wait for 4:58 multiple 4:58 signals. And so essentially, these signals are a way for the outside 5:02 world to send a message into a running workflow that's 5:06 been parked and waiting. 5:09 So what we can do is use 5:11 waitForAny, and we can see here that this 5:15 is a durable wait for whichever of several conditions happens 5:19 first, a signal, a timer, or child's end. 5:24 So let's jump into what this looks like. 5:25 So the first parameter this takes is the name. 5:29 This name here is the step identifier, same as charge 5:32 payment or reserve stock. 5:35 It gets persisted to the database so the workflow engine knows where to 5:39 resume after a crash, restart, or deploy. 5:42 So this does get saved to our journal 5:45 and is part of our persisted workflow. 5:48 And then the second parameter here is an object where we have 5:52 any number of conditions that can break this wait. 5:56 So here we can list as many as we want to essentially 5:59 tell the workflow when it needs to wake up 6:02 and resume processing once 6:04 again. So here I'm going to add three different 6:07 properties to the object. Which each represent a 6:11 different signal that we want to wait for 6:13 that can break the 6:14 wait. So these properties here can be whatever 6:17 we want. 6:18 And now, in order to actually tell wait for any to 6:21 wait for this 6:22 particular signal, we use 6:24 context.signalWait. 6:28 This lets us actually pass in the underlying signal that we want to wait 6:32 for. 6:33 So what are these signals? Well, these are simply going to be a new 6:37 instance of a class that we create, and these signals allow 6:41 us to then 6:43 emit the very same signal later on outside of the 6:47 workflow. And this is what lets us sort of wake the workflow 6:50 back up 6:51 and resume it at any point. So for this, I want to create a new 6:55 directory called Shipping. 6:58 And this is where we're going to create these new signals. 7:02 And I'll create a file called carrierEvent.ts. 7:07 So in here, we're going to go ahead and 7:11 define 7:13 a new schema using Zod, which is going to define the 7:17 shape of a carrier event. 7:20 A carrier event is essentially an event that is going to be emitted from 7:24 our dummy carrier service and will have an HTTP 7:28 endpoint that actually calls it. So this will act as almost like a web 7:31 hook that gets called, and this is the data that it's going to call us 7:35 with. 7:36 So it will include the 7:38 actual type of the event, which includes shipping in transit, 7:42 delivered or returned, 7:45 the order reference number, which will be the order ID, 7:48 a tracking number, and occurred at, all using Zod schema here. 7:53 Then we can export type CarrierEvent 7:56 using z.infer, 7:59 which then lets us pass in type of CarrierEventSchema. 8:03 So now we have the carrier event typed into a new type. 8:07 I want to export const ShipmentDelivered. 8:11 This is going to be our first workflow signal. 8:14 We create these with new WorkflowSignal from NestJS 8:17 workflow. 8:19 Here, we simply have any string that will identify the workflow signal. 8:24 You can call this whatever you want. 8:25 I'll call this shipment.delivered. 8:28 And importantly, we can provide a type here to include the 8:34 payload along with the workflow signal. 8:37 So here that's going to be the carrier event that we'll pass along with it. 8:41 So now we have a shipment delivered event which allows us to 8:45 wake up the workflow when this is received. 8:48 I'll also create one more, 8:50 ShipmentReturned, in the case that our 8:54 item got returned by the shipper. 8:57 So this will be another workflow signal with CarrierEvent, 9:01 and I'll call this Shipment.Returned. 9:04 So now we have these two workflow signals. 9:06 We need to wire them into this wait for any setup that we 9:10 have. 9:11 So to do this, we now pass in the workflow signal 9:14 instance directly 9:15 into signalWait. This tells it that once it receives 9:19 any signal events with shipment delivered, it will wake up. 9:24 But hold on. How will the workflow signal know to wake 9:27 up the 9:27 correct instance of this workflow, right? 9:30 The one that is for the same carrier event that has 9:34 the order ID? Well, for this, we can also 9:37 attach a second parameter, an object where we specify 9:41 the key. 9:42 And this property, as we can see here, 9:45 says that only signals sent with the same key 9:49 match. 9:51 And so now we'll specify the underlying order.id that this 9:55 workflow instance is associated with, so that when we send 9:59 this shipment delivered carrier event with the key 10:03 of the order ID, which will be that reference, then it will 10:07 wake up the same workflow instance. 10:09 And that's exactly what we want. 10:13 I'll go ahead and do the same thing now for return. 10:15 So if we have a return event issued as 10:19 well, this is another case where we want to wake 10:22 up the workflow 10:23 and we'll do the same key here. So for only for the order ID associated 10:27 with. 10:28 And then the final thing we can do is have a 10:31 time-based signal where I'll call this one 10:35 timedOut. And for this, you can use 10:38 context.timer, which is a special signal, a durable 10:42 timer, 10:43 where it takes in any duration value, and at the end of that, it will emit 10:47 the signal inside of waitForAny. 10:50 So here I'll say three days, for example. 10:54 And that means that after three days, if we have not delivered or returned 10:57 the order, then we will automatically wake this 11:00 workflow back up. 11:01 And so what does this actually mean when we say wake back up? 11:04 Well, remember again, a key component to understand with this 11:08 workflow is 11:08 that all of this data is being stored in the database. 11:12 The instance of the workflow, every step that we execute, 11:16 the result of it is stored in the database as part of that workflow 11:19 journal. And so what happens is when we do send a signal 11:23 or this times out here after three days, 11:26 we re-enter this workflow from the very beginning. 11:30 We rerun this charge context step, set the 11:33 status, rerun reserve stock. 11:36 But however, 11:38 we don't actually re-execute the logic here 11:41 because we've already stored the result of this workflow 11:45 instance inside of the journal. So it doesn't rerun 11:49 it, it just returns back the original result, 11:52 so that when we come 11:53 back here, we end up at the same point in time in 11:56 this code, right at 11:57 line forty-three is now where we are. 12:00 And here we can actually now do something different. 12:03 We are resuming the execution context and are ready to 12:08 do whatever we want inside of this workflow instance. 12:11 So what we're going to do here now is at this point, we know this outcome has 12:15 been resolved. So we can actually check to see 12:18 Outcome.key. Now what this does is it 12:22 actually returns us back the value for 12:25 whichever 12:27 signal was the one that resolved it. 12:29 So here it already knows the three properties we defined, 12:33 delivered, returned, or timed out. 12:35 So whichever one of these signals is the one that actually re-resumed the 12:39 execution will be set to the key here. 12:43 And we can do a check to see if the outcome.key is returned, for 12:46 example. Well, that means we know the order has 12:50 returned 12:51 to the warehouse, and so we want to now fail this workflow 12:55 and end it. To do this, we can use 12:58 context.fail 13:00 and say the carrier returned order, 13:05 order.id 13:06 to the warehouse. 13:09 And critically, as we know, what we-- what this means is when we enter in a 13:13 failed state, we are going to now undo the workflow 13:16 steps. 13:17 So we're gonna go in reverse order here, and any steps that have been executed, we 13:21 execute their compensating action. 13:23 So here we're going to release the inventory since it's now back in our 13:26 warehouse, 13:27 and we will refund the user because they never actually got the product. 13:31 So that is a key benefit, is being able to then roll this back at 13:34 any point in time 13:35 and essentially having this atomic outcome for our business operations, which is 13:39 really cool. Otherwise, let's check to see if outcome.key 13:43 was equal to timed out. 13:46 We will say 13:48 context.fail 13:50 order, 13:51 order ID 13:53 was not delivered within 13:56 three days, for example. And you can make this configurable 14:00 or increase it as your application might need, or you might not even have a 14:04 timeout at all, and this can just run forever, and 14:06 that would be okay. 14:08 However, at this point, we know that we have reached the delivered 14:12 state. For this, I'm gonna destructure delivery as 14:16 outcome.value. And what this is 14:20 is actually the data that got passed along with 14:24 the actual signal. So it knows here that the outcome.value 14:28 now is the carrier event that we added to 14:32 the workflow signal. So it has all these other properties on 14:36 it, 14:36 and now we can get access to that. 14:40 So now what I wanna do is use context.commit. 14:43 And as we can see here, 14:45 context.commit is point of no return. 14:48 Compensation of the steps called before it are discarded. 14:51 Any later failure or cancel runs only the compensation of steps 14:55 called after it. So essentially, this is a point of no return where 14:59 we say any step that happened before, we never want 15:03 to undo it, and that's exactly what we want. 15:05 At this point, we know the user got their product, 15:08 so we never want 15:09 to release it or refund them. So we're gonna use 15:13 context.commit here 15:15 and set the commit name to delivered, so it updates our journal with this 15:19 commit. 15:21 We can also set status to delivered as 15:25 well for debugging. And now at this point, 15:29 we wanna do something else. We wanna actually pause the workflow 15:33 again and wait a bit of time and then ask the 15:37 user to review their order. 15:39 To do this, we can tell the workflow to enter a sleep 15:43 state 15:43 on its own, 15:45 like we did for context.timer. 15:48 We can enter this sleep state for a fixed amount of time and 15:52 say here we want to context.sleep. 15:55 Again, give it a name so that it is in the journal with a name that we can 15:59 identify. 16:01 We'll call this before review request, and I'll set this to 16:05 whatever review delay period we want. 16:07 For example, I'll say after seven days, we'll resume this 16:10 and then ask the user for 16:11 a review. 16:13 And now at this point, we're gonna use await context.step. 16:17 So a key component to understand is after seven days passes, 16:21 this workflow instance will automatically be resumed from the beginning. 16:25 Again, we will pass over all of these steps 16:27 that are already in the journal. 16:29 Wait for any will immediately return because it already has a signal 16:34 that has been met. 16:36 And we're back at this point in the code. 16:39 And so here I now want to enter another step 16:42 called send review request. 16:46 And we're going to 16:48 destructure the idempotency key again from the step context. 16:52 And we will use the mail service now .send review 16:56 request and pass in the order and the idempotency key. 17:00 This does not need a compensating action because there's nothing to 17:04 undo here. 17:06 And then finally, context.setstatus back to review 17:09 requested. So this is really nice. 17:11 We can see this business logic in action where we have these periods 17:15 of time in between the workflow, and then we're able to resume it 17:19 automatically with no side effects. 17:22 The final step is to return from the workflow, 17:25 which is the properties expected. So that'll be the charge ID. 17:30 We'll use our charge.chargeID from the first step. 17:34 And then the tracking number for the order comes from the 17:36 delivery.trackingNumber. And so 17:40 this is amazing. We now have a fully completed workflow. 17:44 And hopefully you can start to understand this execution model. 17:47 It definitely takes a bit of time to understand that we are 17:51 exiting and resuming this same function, this same bit of 17:54 code as our signals come in or time 17:57 expires. And it's because we've already journaled 18:00 these steps, each 18:02 one, that we can replay them without actually 18:04 redoing the underlying 18:06 functions. Make sure back in your root app module, 18:10 we add it to our providers array, the order fulfillment workflow. 18:14 And now our application is fully working and wired in with this 18:18 new workflow, 18:18 which is awesome. Let's see how we can now actually invoke 18:22 it 18:22 and critically also wire in these signals 18:26 to resume the workflow at the correct point in time.