Blog

All Blog Posts  |  Next Post  |  Previous Post

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:
//We will query two endpoints:
//https://restcountries.com/v3.1/name/{CountryName}
//https://restcountries.com/v3.1/alpha/{CountryCode}
//Create a common request method that we can reuse
function TForm1.ExecuteRestCountryRequest(APath: string; ACountryOrCode: string): IPromise<string>;
begin
  Result := Promise.New<string>(procedure (AResolve: TProc<string>; AReject: TProc<Exception>)
  begin
    c.Request.Clear;
    c.Request.Host := 'https://restcountries.com/v3.1';
    c.Request.Method := rmGET;
    c.Request.Path := APath + '/' + ACountryOrCode;
    c.Request.ResultType := rrtString;
    c.ExecuteRequest(procedure (const ARequestResult: TTMSFNCCloudBaseRequestResult)
    begin
      if ARequestResult.Success then
        AResolve(ARequestResult.ResultString)
      else
        AReject(Exception.Create('Request failed. No country or code found: ' + ACountryOrCode));
    end);
  end);
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:
function TForm1.GetCountryNameFromCode(ACode: string): IPromise<string>;
begin
  Result := ExecuteRestCountryRequest('/alpha', ACode)
  .ThenBy(function (const AValue: string): string
  begin
    //AValue contains the JSON response, parse it to get the name:
    Result := GetCountryNameFromJSON(value);
  end);
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:
Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
begin
  //Do the first promisified call
end)
.ThenBy(function(const AResult: TVoid): IPromise<TVoid>
begin
  Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
  begin
    //Do the second promisified call
  end);
end)
.ThenBy(function(const AResult: TVoid): IPromise<TVoid>
begin
  Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
  begin
    //Do the third promisified call
  end);
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:
procedure TForm1.GetBorderingCountires(ACountry: string);
begin
  //Start by getting the country name
  ExecuteRestCountryRequest('/name', ACountry)
  .Op.ThenBy<TArray<string>>(function (const AValue: string): IPromise<TArray<string>>
  var
    LBorders: TArray<string>;
    LPromises: TArray<IPromise<string>>;
    I, LBorderCount: Integer;
  begin
    //Parse the returned JSON to retrieve the list of
	//bordering countries
    LBorders := GetBorderingCountriesFromJSON(AValue);
    LBorderCount := Length(LBorders);
    SetLength(LPromises, LBorderCount);

    //Create a promise for each country code, these are individual
	//requests:
    for I := 0 to LBorderCount - 1 do
      LPromises[I] := GetCountryNameFromCode(LBorders[I]);

    //Wait for all the promises to complete
    Result := Promise.All<string>(LPromises);
  end)
  .Main.ThenBy<TVoid>(function (const AValues: TArray<string>): TVoid
  var
    I: Integer;
  begin
    //Cautiously update the UI reflecting the list:
    if FFormNotDestroyed then
    begin
      for I := 0 to Length(AValues) - 1 do
        Memo1.Lines.Add(AValues[I]);
    end;

    Result := Void;
  end)
  .Main.Catch(procedure (E: Exception)
  begin
    //Show errors - if any:
    ShowMessage(E.Message);
  end);
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




This blog post has received 5 comments.


1. Wednesday, December 18, 2024 at 1:01:24 PM

Wow, this looks like it will simplify a lot of code I''ve got in a WEB Core app! Does it also mean we can do away with [async] tags and await(...) calls?

wiz@thetoolwiz.com


2. Friday, December 20, 2024 at 10:39:29 AM

It seems their origial code from github relies on things that are not supported in D10.4, yet it says above that D10.2 or later should suffice.

wiz@thetoolwiz.com


3. Friday, December 20, 2024 at 11:15:56 AM

Sorry, this may have to do with WEB Core''s pas2js. Do you have a version of the Promises lib that works in WEB Core? (How''s it working in the Cloud lib with WEB Core?)

wiz@thetoolwiz.com


4. Friday, December 20, 2024 at 11:23:27 AM

Promises already exist in WEB Core for a long time, there was/is no need to bring this to WEB Core. It was brought to native apps here.

Bruno Fierens


5. Friday, December 20, 2024 at 11:23:49 AM

We adapted the code to make it work with 10.2 and newer

Bruno Fierens




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