Blog

All Blog Posts  |  Next Post  |  Previous Post

TMS Software Delphi  Components TMS Software Delphi  Components Sequential Code for Async Cloud Requests with Delphi Promises!

Tuesday, December 17, 2024

Asynchronous programming is crucial for modern applications, but managing multiple callbacks and events can quickly turn into a tangled mess—commonly known as "callback hell". But the very need for asynchronous calls is to avoid GUI thread freezes when for example HTTP requests take an unpredictable amount of time. To tackle this challenge, we are excited to introduce beta support for promises in TMS FNC Core! With promises, you can write clean, sequential code for cloud-based operations while maintaining all the benefits of asynchronous behavior.

The ChallengeTMS Software Delphi  Components

TMS FNC Cloud Pack relies on asynchronous requests to communicate with various cloud services, such as Google, PayPal, Stellards.io, and more. If you've worked with our components, you’re familiar with how asynchronous communication is handled via callbacks and events.

While effective, this approach can become difficult to manage. For example, to view an event from the Google Calendar API, you need to go through all these steps:
  1. Authenticate the user.
  2. Retrieve a list of available calendars.
  3. Fetch the events for a specific calendar.
Using callbacks and events for such workflows can quickly spiral into complexity, making the code harder to read, maintain and debug.

Promising Solution

Being so used to using promises in the context of web applications, where by design, several functions work asynchronously, we missed a promises implementation for a long time for native Delphi code. Especially in the area of using REST API functions, the ability to use promises significantly simplifies writing code that needs to be built with chained asynchronous calls. 

It was with much enthusiasm that we discovered the promises library written by Laurens van Run from MendriX earlier this year. This triggered us to get in touch with Laurens and see how we could both spread the message about this fantastic new Delphi promises library and also leverage this in the context of our components, especially the TMS FNC Cloud Pack that is built on top of asynchronous REST APIs. 

By working together with Laurens, we also contributed to make his promises library cross-platform and offer it integrated as part of TMS FNC Core to make using it from TMS FNC Cloud Pack as seamless as possible. 

We thank Laurens so much for all his efforts that went into this. Laurens did a great job for the Delphi community and his promises library is not the only one. Laurens also actively contributes to projects like DelphiCodeCoverage and Delphi-Mocks.  And Laurens also revitalized the (old) Delphi SonarQube plugin and worked with Embarcadero to bring attention to it. 

Together with his colleagues at Mendrix and the Delphi community, Laurens drives forward innovation in the Delphi world. Check also what jobs Mendrix has to offer for Delphi developers here: https://werkenbijmendrix.nl/


Example

Let’s break down a small example to understand the building blocks of using promises. In this example, we’ll request the bordering countries of a given country using the REST Countries API. The API returns an array of country codes for the bordering nations, so we’ll need to make additional requests to retrieve their full names.

1. Deferred Promises
The core mechanism relies on deferred promises, which are created using Promise.New. This method takes an executor function as a parameter. The executor function provides two parameters, AResolve and AReject, which allow you to resolve or reject a promise after an asynchronous operation completes. These parameters can be persisted and invoked later or used in a callback directly:
  1. //We will query two endpoints:  
  2. //https://restcountries.com/v3.1/name/{CountryName}  
  3. //https://restcountries.com/v3.1/alpha/{CountryCode}  
  4. //Create a common request method that we can reuse  
  5. function TForm1.ExecuteRestCountryRequest(APath: string; ACountryOrCode: string): IPromise<string>;  
  6. begin  
  7.   Result := Promise.New<string>(procedure (AResolve: TProc<string>; AReject: TProc<Exception>)  
  8.   begin  
  9.     c.Request.Clear;  
  10.     c.Request.Host := 'https://restcountries.com/v3.1';  
  11.     c.Request.Method := rmGET;  
  12.     c.Request.Path := APath + '/' + ACountryOrCode;  
  13.     c.Request.ResultType := rrtString;  
  14.     c.ExecuteRequest(procedure (const ARequestResult: TTMSFNCCloudBaseRequestResult)  
  15.     begin  
  16.       if ARequestResult.Success then  
  17.         AResolve(ARequestResult.ResultString)  
  18.       else  
  19.         AReject(Exception.Create('Request failed. No country or code found: ' + ACountryOrCode));  
  20.     end);  
  21.   end);  
  22. end;  

2. ChainingTMS Software Delphi  Components
Chaining ensures sequential execution of operations while preserving asynchronous behavior. Each .ThenBy call returns a new IPromise<T> and defines what happens after the current promise resolves. The value returned from each step is passed to the next in the chain, reducing the need for nested callbacks.

A simple example on chaining:
  1. function TForm1.GetCountryNameFromCode(ACode: string): IPromise<string>;  
  2. begin  
  3.   Result := ExecuteRestCountryRequest('/alpha', ACode)  
  4.   .ThenBy(function (const AValue: string): string  
  5.   begin  
  6.     //AValue contains the JSON response, parse it to get the name:  
  7.     Result := GetCountryNameFromJSON(value);  
  8.   end);  
  9. end;  
In the snippet above, ExecuteRestCountryRequest is used to make an API call, and the response is parsed in a chained step to extract the desired data—in this case, the country name.

3. Chain multiple deferred promises
Understanding this step is important before moving on to the final step of waiting for multiple deferred promises to complete.
It might seem a bit complex at first glance, but combining multiple deferred promises with chaining is actually simple. The key to understanding this is that a promise is resolved when the Result is set. This means we need to return a promise from the .ThenBy method, ensuring that the chain continues only after the current promise has been resolved.

Here’s how it works in practice:
  1. Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)  
  2. begin  
  3.   //Do the first promisified call  
  4. end)  
  5. .ThenBy(function(const AResult: TVoid): IPromise<TVoid>  
  6. begin  
  7.   Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)  
  8.   begin  
  9.     //Do the second promisified call  
  10.   end);  
  11. end)  
  12. .ThenBy(function(const AResult: TVoid): IPromise<TVoid>  
  13. begin  
  14.   Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)  
  15.   begin  
  16.     //Do the third promisified call  
  17.   end);  
  18. end);  

4. Wait for multiple promises
Promise.All allows us to wait for multiple promises to resolve, and we can apply the same logic as before. The key difference is that, instead of returning a single IPromise<T>, we now return an array of them:
  1. procedure TForm1.GetBorderingCountires(ACountry: string);  
  2. begin  
  3.   //Start by getting the country name  
  4.   ExecuteRestCountryRequest('/name', ACountry)  
  5.   .Op.ThenBy<TArray<string>>(function (const AValue: string): IPromise<TArray<string>>  
  6.   var  
  7.     LBorders: TArray<string>;  
  8.     LPromises: TArray<IPromise<string>>;  
  9.     I, LBorderCount: Integer;  
  10.   begin  
  11.     //Parse the returned JSON to retrieve the list of  
  12.     //bordering countries  
  13.     LBorders := GetBorderingCountriesFromJSON(AValue);  
  14.     LBorderCount := Length(LBorders);  
  15.     SetLength(LPromises, LBorderCount);  
  16.   
  17.     //Create a promise for each country code, these are individual  
  18.     //requests:  
  19.     for I := 0 to LBorderCount - 1 do  
  20.       LPromises[I] := GetCountryNameFromCode(LBorders[I]);  
  21.   
  22.     //Wait for all the promises to complete  
  23.     Result := Promise.All<string>(LPromises);  
  24.   end)  
  25.   .Main.ThenBy<TVoid>(function (const AValues: TArray<string>): TVoid  
  26.   var  
  27.     I: Integer;  
  28.   begin  
  29.     //Cautiously update the UI reflecting the list:  
  30.     if FFormNotDestroyed then  
  31.     begin  
  32.       for I := 0 to Length(AValues) - 1 do  
  33.         Memo1.Lines.Add(AValues[I]);  
  34.     end;  
  35.   
  36.     Result := Void;  
  37.   end)  
  38.   .Main.Catch(procedure (E: Exception)  
  39.   begin  
  40.     //Show errors - if any:  
  41.     ShowMessage(E.Message);  
  42.   end);  
  43. end;  
And that’s it! By calling GetBorderingCountries from a button click, you’ll fetch the list of neighboring countries while keeping the UI responsive.

Get Started and Share Your Feedback

If you'd like to see something more practical, particularly in combination with our TMS FNC Cloud Pack components, take a look at the TTMSFNCCloudGoogleCalendar demo we’ve prepared, available under the Demo\Promises folder! It not only demonstrates the concepts discussed above but also shows how to keep your codebase cleaner by inheriting from TTMSFNCCloudGoogleCalendar and handling promises internally.

While awaiting that Christmas dinner to bake in the oven, take a moment to explore how promises can simplify your workflows and make your code more maintainable. The implementation of promises is now available for registered users, integrated into TMS FNC Core as a BETA! To make the most of the Delphi-Promises library, be sure to check out the documentation, which offers detailed guidance to help you get started quickly.

The integration requires Delphi 10.2 or later, so ensure your development environment meets the minimum requirements.

Your feedback will play a valuable role in shaping the future of promises within our FNC range. Dive into the BETA, try it out, and let us know how promises are impacting your development experience!


Tunde Keller




TMS Software Delphi  Components

This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post