Search code examples
delphiasynchronousshopee

What's the correct way to bring an asynchronous task back to main thread?


I'm developing a Wrapper for the Shopee API as a Delphi package, it's open source and available on my gh profile. I already ran into a problem similar to this one which was answered here. But now I can't see the way around anymore.

It starts here:

constructor TShopeeContext.Create(AMode, AAPIKey, APartnerID: string);
begin

  if (AMode <> TEST_SHOPEE) and (AMode <> PRODUCTION_SHOPEE_BR) then
    raise Exception.Create('Mode Invalid for Context');

  FHost := AMode;
  FAPI_Key := AAPIKey;
  FPartnerID := APartnerID;

  UpdateInternalConfigFields;

  if FRefreshToken = '' then
    FHasRefreshToken := False
  else
    FHasRefreshToken := True;

  // Checks if the authorization has been granted and is younger than 365 days.
  if (not FAuthorizationGranted) or ((Now - FAuthorizationDate) > 365) then
  begin
    Authorize;
  end;

end;

After the Authorize; call, any instruction dependent on Authorization Code falls into Undefined Behavior because there's no guarantee I have a code. I would need to wait for Authorize to return before following on. My issue arises from the fact that Authorize creates an object and starts a routine that is Asynchronous, a HTTP listener:

procedure TShopeeContext.Authorize;
var
  Authorizator: TShopeeAuthorizator;
begin
  // Request Authorization;
  Authorizator := TShopeeAuthorizator.Create(FHost, FAPI_Key, FPartnerID, 8342);
  try
    Authorizator.AuthorizationRequest;
  finally
    Authorizator.Free;
  end;

end;

The AuthorizationRequest procedure is implemented as:

procedure TShopeeAuthorizator.AuthorizationRequest;
begin
  // Obtem a assinatura da chamada
  FSignature := GetPublicSignString(API_PATH_AUTHORIZATION, FTimestamp);

  // Constroi os parametros
  FRequestParameters.AddItem('partner_id', FPartnerID);
  FRequestParameters.AddItem('timestamp', FTimeStamp);
  FRequestParameters.AddItem('sign', FSignature);
  FRequestParameters.AddItem('redirect', REDIRECT_URL_WS+':'+IntToStr(FPort));

  ShellExecute(0, 'open', PChar(GenerateRequestURL(API_PATH_AUTHORIZATION, FRequestParameters)), nil, nil, SW_SHOWNORMAL);

  FCatcherServer := TCatcherServer.Create();
  FCatcherServer.OnFieldsReady := FieldsReadyHandler;
  FCatcherServer.Listen(FPort);
end;

and FieldsReadyHandler is the subject of my older question, and the current version is:

procedure TShopeeAuthorizator.FieldsReadyHandler(Sender: TObject; Code,
  AuthorizatedID: string);
begin
  // Handle Code, Auth Type and AuthorizatedID.
  FConfigurator := TConfiguratorFile.Create;
  try
    FConfigurator.SaveAuthorizationInfo(Code, AuthorizatedID, (Sender as TCatcherServer).AuthorizationType);
    FSuccess := True;
  finally
    FConfigurator.Free;
  end;

  TThread.Queue(nil, procedure
  begin
    Sender.Free;
  end);
end;

Usually it's a matter of HOW to do that, but honestly at this point I'm thinking that I overcomplicated it and don't even know WHAT I should do to overcome this. Anyways, if I were to rewrite I can see my self falling in the same spot where my subsequent tasks depends on the async task to finish. I also tried to add an event to inform the Context that it already obtained the code but (my shot is) it raises access violation because of the way the object is released.

Sorry I can't synthesize a TL;DR for this one, but if I had to index my questions the output would be: What is the best approach to this type of problem and how is it handled in pascal? In JS, for example, I would chain infinite .then() until the routine doesn't need the async part anymore. I also feel it is going to become a frequent pain if I don't learn to handle it now as requests are asynchronous.

Thanks in advance.


Solution

  • Reference: Waiting for a Task to Be Completed

    To do this, use an event object. Event objects (System.SyncObjs.TEvent) should be created with global scope so that they can act like signals that are visible to all threads. When a thread completes an operation that other threads depend on, it calls TEvent.SetEvent. SetEvent turns on the signal, so any other thread that checks will know that the operation has completed.

    When your authorization is done, set the event:

    procedure TShopeeAuthorizator.FieldsReadyHandler(Sender: TObject; Code,
      AuthorizatedID: string);
    begin
      // Handle Code, Auth Type and AuthorizatedID.
      FConfigurator := TConfiguratorFile.Create;
      try
        FConfigurator.SaveAuthorizationInfo(Code, AuthorizatedID, (Sender as TCatcherServer).AuthorizationType);
        FSuccess := True;
    
        // Notify 
        Event1.SetEvent;
    
      finally
        FConfigurator.Free;
      end;
    
      TThread.Queue(nil, procedure
      begin
        Sender.Free;
      end);
    end;
    

    And in the TShopeeContext, use the Event to wait until authorization completed, or a given timeout period elapsed:

      if (not FAuthorizationGranted) or ((Now - FAuthorizationDate) > 365) then
      begin
        Event1.ResetEvent; { clear the event before }
        
        Authorize;
    
        if Event1.WaitFor(20000) <> wrSignaled then
          raise Exception; 
      end;       
    

    Notes:

    • As a minor suggestion, I would not recommend to place the authorization code in the constructor of TShopeeContext. Instead, move it to a separate method, and keep the constructor small so it mainly initializes fields.
    • I have not yet used TEvent in my own code. There exist other ways to implement the signalling, which may have advantages over TEvent.