Tina4

Chapter 7: WebSockets#

Real-Time Without Polling#

Your Delphi application displays a dashboard with live metrics. The user stares at the numbers. Nothing moves. They click refresh. The numbers update. They stare again. They click refresh again.

This is HTTP. You ask, the server answers, the connection closes. If you want fresh data, you ask again. Polling every 5 seconds works, but it is wasteful -- 90% of those requests return the same data. It hammers the server. It wastes bandwidth. And the user still sees data that is up to 5 seconds stale.

WebSocket is the fix. One connection opens. It stays open. The server pushes data the instant it changes. The client pushes messages back. No request/response cycle. No polling interval. No stale data.

TTina4WebSocketClient brings WebSocket to your Delphi FMX applications with RFC 6455 compliance, automatic reconnection, ping/pong keepalive, and event-driven message handling. Drop it on a form, set a URL, connect, and start receiving real-time data.


1. TTina4WebSocketClient Overview#

The WebSocket client handles the full RFC 6455 protocol:

  • Full-duplex communication -- send and receive simultaneously over a single TCP connection
  • Auto-reconnect -- configurable reconnection with backoff when the connection drops
  • Ping/pong keepalive -- automatic heartbeat to detect dead connections
  • Text and binary messages -- handle both message types
  • Event-driven -- OnConnected, OnMessage, OnDisconnected, OnError callbacks

Component Setup#

Drop a TTina4WebSocketClient on your form from the Tina4 palette, or create it at runtime:

pascal
uses  Tina4WebSocketClient;โ€‹var  WSClient: TTina4WebSocketClient;begin  WSClient := TTina4WebSocketClient.Create(Self);  WSClient.URL := 'wss://api.example.com/ws';end;

2. Basic Connection#

Design-Time Configuration#

Set properties in the Object Inspector:

PropertyValue
URLwss://api.example.com/ws
AutoReconnectTrue
ReconnectInterval5000 (ms)
PingInterval30000 (ms)

Runtime Connection#

pascal
procedure TForm1.FormCreate(Sender: TObject);begin  Tina4WebSocket1.URL := 'wss://api.example.com/ws';  Tina4WebSocket1.AutoReconnect := True;  Tina4WebSocket1.ReconnectInterval := 5000;โ€‹  Tina4WebSocket1.OnConnected := WebSocketConnected;  Tina4WebSocket1.OnMessage := WebSocketMessage;  Tina4WebSocket1.OnDisconnected := WebSocketDisconnected;  Tina4WebSocket1.OnError := WebSocketError;โ€‹  Tina4WebSocket1.Connect;end;

Event Handlers#

pascal
procedure TForm1.WebSocketConnected(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Connected';    LabelStatus.TextSettings.FontColor := TAlphaColorRec.Green;  end);end;โ€‹procedure TForm1.WebSocketMessage(Sender: TObject; const AMessage: string);begin  TThread.Synchronize(nil, procedure  begin    MemoMessages.Lines.Add(FormatDateTime('hh:nn:ss', Now) + ' ' + AMessage);  end);end;โ€‹procedure TForm1.WebSocketDisconnected(Sender: TObject; const ACode: Integer;  const AReason: string);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Disconnected: ' + AReason;    LabelStatus.TextSettings.FontColor := TAlphaColorRec.Red;  end);end;โ€‹procedure TForm1.WebSocketError(Sender: TObject; const AError: string);begin  TThread.Synchronize(nil, procedure  begin    MemoMessages.Lines.Add('ERROR: ' + AError);  end);end;

3. Sending Messages#

Send Text#

pascal
procedure TForm1.ButtonSendClick(Sender: TObject);begin  Tina4WebSocket1.Send(EditMessage.Text);  EditMessage.Text := '';end;

Send JSON#

pascal
procedure TForm1.SendJSON;var  Obj: TJSONObject;begin  Obj := TJSONObject.Create;  try    Obj.AddPair('type', 'subscribe');    Obj.AddPair('channel', 'notifications');    Obj.AddPair('userId', TJSONNumber.Create(1001));โ€‹    Tina4WebSocket1.Send(Obj.ToString);  finally    Obj.Free;  end;end;

Check Connection Before Sending#

pascal
procedure TForm1.SafeSend(const AMessage: string);begin  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Send(AMessage)  else    ShowMessage('Not connected to server');end;

4. Auto-Reconnect Behavior#

When the connection drops, TTina4WebSocketClient automatically attempts to reconnect if AutoReconnect is True.

Configuration#

pascal
Tina4WebSocket1.AutoReconnect := True;Tina4WebSocket1.ReconnectInterval := 5000;  // 5 seconds between attempts

Reconnect Flow#

Connected -> Connection Lost -> Wait 5s -> Reconnect Attempt 1  -> Fail -> Wait 5s -> Reconnect Attempt 2  -> Success -> Connected

Handling Reconnection in Code#

pascal
procedure TForm1.WebSocketConnected(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Connected';โ€‹    // Re-subscribe to channels after reconnection    var Sub := TJSONObject.Create;    try      Sub.AddPair('type', 'subscribe');      Sub.AddPair('channel', 'updates');      Tina4WebSocket1.Send(Sub.ToString);    finally      Sub.Free;    end;  end);end;

The OnConnected event fires every time the connection opens -- including after a reconnect. Use it to re-subscribe to channels or re-authenticate.


5. Ping/Pong Keepalive#

WebSocket connections can silently die -- a router drops the connection, a firewall times it out, the server crashes. Without keepalive, your client thinks it is still connected while the connection is actually dead.

TTina4WebSocketClient sends periodic ping frames. If the server does not respond with a pong within a timeout, the connection is considered dead and reconnection begins.

pascal
Tina4WebSocket1.PingInterval := 30000;  // Send ping every 30 seconds

You do not need to handle pings manually. The component manages the ping/pong protocol internally.


6. Binary vs Text Messages#

WebSocket supports two frame types: text and binary. TTina4WebSocketClient handles both.

Text Messages#

Most WebSocket APIs use text frames with JSON payloads:

pascal
procedure TForm1.WebSocketMessage(Sender: TObject; const AMessage: string);var  JSON: TJSONObject;begin  JSON := StrToJSONObject(AMessage);  if Assigned(JSON) then  try    var MsgType := JSON.GetValue<String>('type', '');โ€‹    TThread.Synchronize(nil, procedure    begin      if MsgType = 'price_update' then        UpdatePriceDisplay(JSON)      else if MsgType = 'notification' then        ShowNotification(JSON)      else if MsgType = 'error' then        HandleServerError(JSON);    end);  finally    JSON.Free;  end;end;

Binary Messages#

For binary data (images, files, protocol buffers), use the binary message event:

pascal
procedure TForm1.WebSocketBinaryMessage(Sender: TObject; const AData: TBytes);begin  TThread.Synchronize(nil, procedure  begin    // Save binary data to file    var Stream := TFileStream.Create('received_data.bin', fmCreate);    try      Stream.WriteBuffer(AData[0], Length(AData));    finally      Stream.Free;    end;  end);end;

7. Disconnecting#

Graceful Close#

pascal
procedure TForm1.ButtonDisconnectClick(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  // Prevent reconnection  Tina4WebSocket1.Disconnect;end;

Cleanup on Form Destroy#

pascal
procedure TForm1.FormDestroy(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Disconnect;end;

8. Complete Example: Real-Time Chat Client#

Build a chat application that connects to a WebSocket server, sends and receives messages, and displays them in an HTML renderer with online status.

Form Layout#

Place these components on your form:

  • TTina4WebSocketClient (Tina4WebSocket1)
  • TTina4HTMLRender (HTMLRender1) -- displays chat messages
  • TEdit (EditMessage) -- message input
  • TButton (ButtonSend) -- send button
  • TEdit (EditUsername) -- username input
  • TButton (ButtonConnect) -- connect/disconnect
  • TLabel (LabelStatus) -- connection status

Implementation#

pascal
unit ChatForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Memo, FMX.Layouts,  Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โ€‹type  TFormChat = class(TForm)    Tina4WebSocket1: TTina4WebSocketClient;    HTMLRender1: TTina4HTMLRender;    EditMessage: TEdit;    ButtonSend: TButton;    EditUsername: TEdit;    ButtonConnect: TButton;    LabelStatus: TLabel;    procedure FormCreate(Sender: TObject);    procedure FormDestroy(Sender: TObject);    procedure ButtonConnectClick(Sender: TObject);    procedure ButtonSendClick(Sender: TObject);    procedure EditMessageKeyDown(Sender: TObject; var Key: Word;      var KeyChar: Char; Shift: TShiftState);  private    FChatHistory: TStringList;    FOnlineUsers: TStringList;    FIsConnected: Boolean;    procedure OnWSOpen(Sender: TObject);    procedure OnWSMessage(Sender: TObject; const AMessage: string);    procedure OnWSClose(Sender: TObject; const ACode: Integer;      const AReason: string);    procedure OnWSError(Sender: TObject; const AError: string);    procedure AddChatMessage(const AUser, AMessage, ATime: string;      AIsOwn: Boolean);    procedure RefreshChatDisplay;    procedure UpdateOnlineStatus;  end;โ€‹var  FormChat: TFormChat;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TFormChat.FormCreate(Sender: TObject);begin  FChatHistory := TStringList.Create;  FOnlineUsers := TStringList.Create;  FIsConnected := False;โ€‹  Tina4WebSocket1.AutoReconnect := True;  Tina4WebSocket1.ReconnectInterval := 3000;  Tina4WebSocket1.PingInterval := 25000;โ€‹  Tina4WebSocket1.OnConnected := OnWSOpen;  Tina4WebSocket1.OnMessage := OnWSMessage;  Tina4WebSocket1.OnDisconnected := OnWSClose;  Tina4WebSocket1.OnError := OnWSError;โ€‹  ButtonSend.Enabled := False;  LabelStatus.Text := 'Disconnected';โ€‹  // Show empty chat  RefreshChatDisplay;end;โ€‹procedure TFormChat.FormDestroy(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Disconnect;  FChatHistory.Free;  FOnlineUsers.Free;end;โ€‹procedure TFormChat.ButtonConnectClick(Sender: TObject);begin  if FIsConnected then  begin    Tina4WebSocket1.AutoReconnect := False;    Tina4WebSocket1.Disconnect;  end  else  begin    if EditUsername.Text.Trim.IsEmpty then    begin      ShowMessage('Please enter a username');      Exit;    end;โ€‹    Tina4WebSocket1.URL := 'wss://chat.example.com/ws';    Tina4WebSocket1.AutoReconnect := True;    Tina4WebSocket1.Connect;    LabelStatus.Text := 'Connecting...';  end;end;โ€‹procedure TFormChat.OnWSOpen(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    FIsConnected := True;    LabelStatus.Text := 'Connected';    ButtonConnect.Text := 'Disconnect';    ButtonSend.Enabled := True;    EditUsername.Enabled := False;โ€‹    // Send join message    var JoinMsg := TJSONObject.Create;    try      JoinMsg.AddPair('type', 'join');      JoinMsg.AddPair('username', EditUsername.Text);      Tina4WebSocket1.Send(JoinMsg.ToString);    finally      JoinMsg.Free;    end;  end);end;โ€‹procedure TFormChat.OnWSMessage(Sender: TObject; const AMessage: string);begin  TThread.Synchronize(nil, procedure  var    JSON: TJSONObject;  begin    JSON := StrToJSONObject(AMessage);    if not Assigned(JSON) then Exit;    try      var MsgType := JSON.GetValue<String>('type', '');โ€‹      if MsgType = 'chat' then      begin        var User := JSON.GetValue<String>('username', 'Unknown');        var Text := JSON.GetValue<String>('message', '');        var Time := JSON.GetValue<String>('time', FormatDateTime('hh:nn', Now));        var IsOwn := (User = EditUsername.Text);โ€‹        AddChatMessage(User, Text, Time, IsOwn);      end      else if MsgType = 'user_list' then      begin        FOnlineUsers.Clear;        var Users := JSON.GetValue<TJSONArray>('users');        if Assigned(Users) then          for var I := 0 to Users.Count - 1 do            FOnlineUsers.Add(Users.Items[I].Value);        UpdateOnlineStatus;      end      else if MsgType = 'user_joined' then      begin        var User := JSON.GetValue<String>('username', '');        if not FOnlineUsers.Contains(User) then          FOnlineUsers.Add(User);        AddChatMessage('System', User + ' joined the chat', '', False);        UpdateOnlineStatus;      end      else if MsgType = 'user_left' then      begin        var User := JSON.GetValue<String>('username', '');        var Idx := FOnlineUsers.IndexOf(User);        if Idx >= 0 then          FOnlineUsers.Delete(Idx);        AddChatMessage('System', User + ' left the chat', '', False);        UpdateOnlineStatus;      end;    finally      JSON.Free;    end;  end);end;โ€‹procedure TFormChat.OnWSClose(Sender: TObject; const ACode: Integer;  const AReason: string);begin  TThread.Synchronize(nil, procedure  begin    FIsConnected := False;    LabelStatus.Text := 'Disconnected';    ButtonConnect.Text := 'Connect';    ButtonSend.Enabled := False;    EditUsername.Enabled := True;    FOnlineUsers.Clear;    UpdateOnlineStatus;  end);end;โ€‹procedure TFormChat.OnWSError(Sender: TObject; const AError: string);begin  TThread.Synchronize(nil, procedure  begin    AddChatMessage('System', 'Error: ' + AError, '', False);  end);end;โ€‹procedure TFormChat.ButtonSendClick(Sender: TObject);begin  if EditMessage.Text.Trim.IsEmpty then Exit;  if not Tina4WebSocket1.IsConnected then Exit;โ€‹  var Msg := TJSONObject.Create;  try    Msg.AddPair('type', 'chat');    Msg.AddPair('message', EditMessage.Text.Trim);    Tina4WebSocket1.Send(Msg.ToString);  finally    Msg.Free;  end;โ€‹  EditMessage.Text := '';  EditMessage.SetFocus;end;โ€‹procedure TFormChat.EditMessageKeyDown(Sender: TObject; var Key: Word;  var KeyChar: Char; Shift: TShiftState);begin  if Key = vkReturn then    ButtonSendClick(Sender);end;โ€‹procedure TFormChat.AddChatMessage(const AUser, AMessage, ATime: string;  AIsOwn: Boolean);var  Alignment, BgColor, TextColor: string;begin  if AIsOwn then  begin    Alignment := 'right';    BgColor := '#1abc9c';    TextColor := 'white';  end  else if AUser = 'System' then  begin    Alignment := 'center';    BgColor := '#f0f0f0';    TextColor := '#666';  end  else  begin    Alignment := 'left';    BgColor := '#ecf0f1';    TextColor := '#333';  end;โ€‹  FChatHistory.Add(Format(    '<div style="text-align: %s; margin: 5px 0;">' +    '  <div style="display: inline-block; background: %s; color: %s; ' +    '    padding: 8px 12px; border-radius: 12px; max-width: 70%%;">' +    '    <small><strong>%s</strong> %s</small><br>%s' +    '  </div>' +    '</div>',    [Alignment, BgColor, TextColor, AUser, ATime, AMessage]));โ€‹  RefreshChatDisplay;end;โ€‹procedure TFormChat.RefreshChatDisplay;begin  HTMLRender1.HTML.Text :=    '<div style="font-family: Arial, sans-serif; padding: 10px;">' +    '<h3 style="color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 5px;">Chat</h3>' +    FChatHistory.Text +    '</div>';end;โ€‹procedure TFormChat.UpdateOnlineStatus;begin  // Update status label with online user count  if FOnlineUsers.Count > 0 then    LabelStatus.Text := Format('Connected (%d online)', [FOnlineUsers.Count])  else if FIsConnected then    LabelStatus.Text := 'Connected'  else    LabelStatus.Text := 'Disconnected';end;โ€‹end.

9. Complete Example: Live Data Feed#

Build a real-time price feed that connects to a WebSocket server, receives price updates, and displays them with color-coded changes.

Implementation#

pascal
unit PriceFeedForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes, System.JSON,  System.Generics.Collections, FMX.Types, FMX.Controls, FMX.Forms,  FMX.StdCtrls, FMX.Layouts,  Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โ€‹type  TPriceInfo = record    Symbol: string;    Price: Double;    PrevPrice: Double;    Change: Double;    ChangePercent: Double;    LastUpdate: TDateTime;  end;โ€‹  TFormPriceFeed = class(TForm)    Tina4WebSocket1: TTina4WebSocketClient;    HTMLRender1: TTina4HTMLRender;    ButtonConnect: TButton;    LabelStatus: TLabel;    procedure FormCreate(Sender: TObject);    procedure FormDestroy(Sender: TObject);    procedure ButtonConnectClick(Sender: TObject);  private    FPrices: TDictionary<string, TPriceInfo>;    procedure OnWSOpen(Sender: TObject);    procedure OnWSMessage(Sender: TObject; const AMessage: string);    procedure OnWSClose(Sender: TObject; const ACode: Integer;      const AReason: string);    procedure OnWSError(Sender: TObject; const AError: string);    procedure UpdatePriceDisplay;    function GetChangeColor(AChange: Double): string;    function GetChangeArrow(AChange: Double): string;  end;โ€‹var  FormPriceFeed: TFormPriceFeed;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TFormPriceFeed.FormCreate(Sender: TObject);begin  FPrices := TDictionary<string, TPriceInfo>.Create;โ€‹  Tina4WebSocket1.URL := 'wss://feeds.example.com/prices';  Tina4WebSocket1.AutoReconnect := True;  Tina4WebSocket1.ReconnectInterval := 5000;  Tina4WebSocket1.PingInterval := 20000;โ€‹  Tina4WebSocket1.OnConnected := OnWSOpen;  Tina4WebSocket1.OnMessage := OnWSMessage;  Tina4WebSocket1.OnDisconnected := OnWSClose;  Tina4WebSocket1.OnError := OnWSError;โ€‹  LabelStatus.Text := 'Disconnected';  UpdatePriceDisplay;end;โ€‹procedure TFormPriceFeed.FormDestroy(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Disconnect;  FPrices.Free;end;โ€‹procedure TFormPriceFeed.ButtonConnectClick(Sender: TObject);begin  if Tina4WebSocket1.IsConnected then  begin    Tina4WebSocket1.AutoReconnect := False;    Tina4WebSocket1.Disconnect;    ButtonConnect.Text := 'Connect';  end  else  begin    Tina4WebSocket1.AutoReconnect := True;    Tina4WebSocket1.Connect;    LabelStatus.Text := 'Connecting...';    ButtonConnect.Text := 'Disconnect';  end;end;โ€‹procedure TFormPriceFeed.OnWSOpen(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Connected - Receiving prices';โ€‹    // Subscribe to price updates    var Sub := TJSONObject.Create;    try      Sub.AddPair('type', 'subscribe');      var Symbols := TJSONArray.Create;      Symbols.Add('BTC-USD');      Symbols.Add('ETH-USD');      Symbols.Add('AAPL');      Symbols.Add('GOOGL');      Symbols.Add('MSFT');      Sub.AddPair('symbols', Symbols);      Tina4WebSocket1.Send(Sub.ToString);    finally      Sub.Free;    end;  end);end;โ€‹procedure TFormPriceFeed.OnWSMessage(Sender: TObject; const AMessage: string);begin  TThread.Synchronize(nil, procedure  var    JSON: TJSONObject;    Info: TPriceInfo;  begin    JSON := StrToJSONObject(AMessage);    if not Assigned(JSON) then Exit;    try      var MsgType := JSON.GetValue<String>('type', '');โ€‹      if MsgType = 'price' then      begin        var Symbol := JSON.GetValue<String>('symbol', '');        var NewPrice := JSON.GetValue<Double>('price', 0);โ€‹        if FPrices.TryGetValue(Symbol, Info) then        begin          Info.PrevPrice := Info.Price;          Info.Price := NewPrice;          Info.Change := NewPrice - Info.PrevPrice;          if Info.PrevPrice > 0 then            Info.ChangePercent := (Info.Change / Info.PrevPrice) * 100          else            Info.ChangePercent := 0;        end        else        begin          Info.Symbol := Symbol;          Info.Price := NewPrice;          Info.PrevPrice := NewPrice;          Info.Change := 0;          Info.ChangePercent := 0;        end;โ€‹        Info.LastUpdate := Now;        FPrices.AddOrSetValue(Symbol, Info);        UpdatePriceDisplay;      end;    finally      JSON.Free;    end;  end);end;โ€‹procedure TFormPriceFeed.OnWSClose(Sender: TObject; const ACode: Integer;  const AReason: string);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Disconnected - Reconnecting...';    ButtonConnect.Text := 'Connect';  end);end;โ€‹procedure TFormPriceFeed.OnWSError(Sender: TObject; const AError: string);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Error: ' + AError;  end);end;โ€‹function TFormPriceFeed.GetChangeColor(AChange: Double): string;begin  if AChange > 0 then    Result := '#27ae60'  else if AChange < 0 then    Result := '#e74c3c'  else    Result := '#666';end;โ€‹function TFormPriceFeed.GetChangeArrow(AChange: Double): string;begin  if AChange > 0 then    Result := '&#9650;'  // up triangle  else if AChange < 0 then    Result := '&#9660;'  // down triangle  else    Result := '&#9644;'; // dashend;โ€‹procedure TFormPriceFeed.UpdatePriceDisplay;var  HTML: TStringBuilder;  Pair: TPair<string, TPriceInfo>;begin  HTML := TStringBuilder.Create;  try    HTML.AppendLine('<div style="font-family: Arial, sans-serif; padding: 15px;">');    HTML.AppendLine('<h2 style="color: #2c3e50;">Live Price Feed</h2>');    HTML.AppendLine('<table style="width: 100%; border-collapse: collapse;">');    HTML.AppendLine('<thead>');    HTML.AppendLine('<tr style="background: #2c3e50; color: white;">');    HTML.AppendLine('  <th style="padding: 10px; text-align: left;">Symbol</th>');    HTML.AppendLine('  <th style="padding: 10px; text-align: right;">Price</th>');    HTML.AppendLine('  <th style="padding: 10px; text-align: right;">Change</th>');    HTML.AppendLine('  <th style="padding: 10px; text-align: right;">%</th>');    HTML.AppendLine('  <th style="padding: 10px; text-align: right;">Updated</th>');    HTML.AppendLine('</tr>');    HTML.AppendLine('</thead>');    HTML.AppendLine('<tbody>');โ€‹    for Pair in FPrices do    begin      var Color := GetChangeColor(Pair.Value.Change);      var Arrow := GetChangeArrow(Pair.Value.Change);โ€‹      HTML.AppendFormat(        '<tr style="border-bottom: 1px solid #eee;">' +        '  <td style="padding: 10px; font-weight: bold;">%s</td>' +        '  <td style="padding: 10px; text-align: right; font-size: 1.1em;">$%.2f</td>' +        '  <td style="padding: 10px; text-align: right; color: %s;">%s %.2f</td>' +        '  <td style="padding: 10px; text-align: right; color: %s;">%.2f%%</td>' +        '  <td style="padding: 10px; text-align: right; color: #999; font-size: 0.85em;">%s</td>' +        '</tr>',        [Pair.Value.Symbol, Pair.Value.Price, Color, Arrow,         Abs(Pair.Value.Change), Color, Pair.Value.ChangePercent,         FormatDateTime('hh:nn:ss', Pair.Value.LastUpdate)]);    end;โ€‹    HTML.AppendLine('</tbody>');    HTML.AppendLine('</table>');โ€‹    if FPrices.Count = 0 then      HTML.AppendLine('<p style="color: #999; text-align: center; padding: 40px;">Waiting for price data...</p>');โ€‹    HTML.AppendLine('</div>');โ€‹    HTMLRender1.HTML.Text := HTML.ToString;  finally    HTML.Free;  end;end;โ€‹end.

10. Exercise: Notification System#

Build a notification system with these requirements:

  1. Connect to a WebSocket server at wss://api.example.com/notifications
  2. Receive notifications as JSON with id, title, message, type (info, warning, error), and timestamp fields
  3. Display notifications as toast-style messages in an HTML renderer
  4. Show a badge counter of unread notifications
  5. Clicking a notification marks it as read (send {"type": "mark_read", "id": "..."} back to the server)
  6. Notifications fade out after 10 seconds unless marked as read manually

Solution#

pascal
unit NotificationForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes, System.JSON,  System.Generics.Collections,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Layouts,  FMX.Objects,  Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โ€‹type  TNotification = record    ID: string;    Title: string;    Message: string;    NotifType: string;  // info, warning, error    Timestamp: TDateTime;    IsRead: Boolean;  end;โ€‹  TFormNotifications = class(TForm)    Tina4WebSocket1: TTina4WebSocketClient;    HTMLRender1: TTina4HTMLRender;    LabelBadge: TLabel;    LabelStatus: TLabel;    TimerFade: TTimer;    procedure FormCreate(Sender: TObject);    procedure FormDestroy(Sender: TObject);    procedure TimerFadeTimer(Sender: TObject);  private    FNotifications: TList<TNotification>;    procedure OnWSOpen(Sender: TObject);    procedure OnWSMessage(Sender: TObject; const AMessage: string);    procedure OnWSClose(Sender: TObject; const ACode: Integer;      const AReason: string);    procedure RefreshDisplay;    procedure UpdateBadge;    procedure MarkAsRead(const AID: string);    function GetTypeColor(const AType: string): string;    function GetTypeIcon(const AType: string): string;    function UnreadCount: Integer;  end;โ€‹var  FormNotifications: TFormNotifications;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TFormNotifications.FormCreate(Sender: TObject);begin  FNotifications := TList<TNotification>.Create;โ€‹  Tina4WebSocket1.URL := 'wss://api.example.com/notifications';  Tina4WebSocket1.AutoReconnect := True;  Tina4WebSocket1.ReconnectInterval := 5000;  Tina4WebSocket1.PingInterval := 30000;  Tina4WebSocket1.OnConnected := OnWSOpen;  Tina4WebSocket1.OnMessage := OnWSMessage;  Tina4WebSocket1.OnDisconnected := OnWSClose;โ€‹  // Timer checks for notifications older than 10 seconds  TimerFade.Interval := 2000;  TimerFade.Enabled := True;โ€‹  // Register click handler for mark-as-read  HTMLRender1.RegisterObject('Notif', Self);โ€‹  Tina4WebSocket1.Connect;  LabelStatus.Text := 'Connecting...';  UpdateBadge;  RefreshDisplay;end;โ€‹procedure TFormNotifications.FormDestroy(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Disconnect;  FNotifications.Free;end;โ€‹procedure TFormNotifications.OnWSOpen(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Connected';  end);end;โ€‹procedure TFormNotifications.OnWSMessage(Sender: TObject; const AMessage: string);begin  TThread.Synchronize(nil, procedure  var    JSON: TJSONObject;    Notif: TNotification;  begin    JSON := StrToJSONObject(AMessage);    if not Assigned(JSON) then Exit;    try      var MsgType := JSON.GetValue<String>('type', '');โ€‹      if (MsgType = 'notification') or (MsgType = 'info') or         (MsgType = 'warning') or (MsgType = 'error') then      begin        Notif.ID := JSON.GetValue<String>('id', GetGUID);        Notif.Title := JSON.GetValue<String>('title', 'Notification');        Notif.Message := JSON.GetValue<String>('message', '');        Notif.NotifType := JSON.GetValue<String>('type', 'info');        Notif.Timestamp := Now;        Notif.IsRead := False;โ€‹        FNotifications.Insert(0, Notif);  // Newest first        UpdateBadge;        RefreshDisplay;      end;    finally      JSON.Free;    end;  end);end;โ€‹procedure TFormNotifications.OnWSClose(Sender: TObject; const ACode: Integer;  const AReason: string);begin  TThread.Synchronize(nil, procedure  begin    LabelStatus.Text := 'Reconnecting...';  end);end;โ€‹procedure TFormNotifications.TimerFadeTimer(Sender: TObject);var  Changed: Boolean;  I: Integer;begin  Changed := False;โ€‹  // Remove unread notifications older than 10 seconds  for I := FNotifications.Count - 1 downto 0 do  begin    var Notif := FNotifications[I];    if (not Notif.IsRead) and (SecondsBetween(Now, Notif.Timestamp) > 10) then    begin      Notif.IsRead := True;      FNotifications[I] := Notif;      Changed := True;    end;  end;โ€‹  if Changed then  begin    UpdateBadge;    RefreshDisplay;  end;end;โ€‹procedure TFormNotifications.MarkAsRead(const AID: string);var  I: Integer;begin  for I := 0 to FNotifications.Count - 1 do  begin    var Notif := FNotifications[I];    if Notif.ID = AID then    begin      Notif.IsRead := True;      FNotifications[I] := Notif;โ€‹      // Notify the server      if Tina4WebSocket1.IsConnected then      begin        var Msg := TJSONObject.Create;        try          Msg.AddPair('type', 'mark_read');          Msg.AddPair('id', AID);          Tina4WebSocket1.Send(Msg.ToString);        finally          Msg.Free;        end;      end;โ€‹      UpdateBadge;      RefreshDisplay;      Break;    end;  end;end;โ€‹function TFormNotifications.UnreadCount: Integer;var  I: Integer;begin  Result := 0;  for I := 0 to FNotifications.Count - 1 do    if not FNotifications[I].IsRead then      Inc(Result);end;โ€‹procedure TFormNotifications.UpdateBadge;var  Count: Integer;begin  Count := UnreadCount;  if Count > 0 then  begin    LabelBadge.Text := Count.ToString;    LabelBadge.Visible := True;  end  else    LabelBadge.Visible := False;end;โ€‹function TFormNotifications.GetTypeColor(const AType: string): string;begin  if AType = 'error' then    Result := '#e74c3c'  else if AType = 'warning' then    Result := '#f39c12'  else    Result := '#3498db';end;โ€‹function TFormNotifications.GetTypeIcon(const AType: string): string;begin  if AType = 'error' then    Result := '&#10060;'  else if AType = 'warning' then    Result := '&#9888;'  else    Result := '&#8505;';end;โ€‹procedure TFormNotifications.RefreshDisplay;var  HTML: TStringBuilder;  Notif: TNotification;begin  HTML := TStringBuilder.Create;  try    HTML.AppendLine('<div style="font-family: Arial, sans-serif; padding: 10px;">');    HTML.AppendLine('<h3 style="color: #2c3e50;">Notifications</h3>');โ€‹    if FNotifications.Count = 0 then    begin      HTML.AppendLine('<p style="color: #999; text-align: center; padding: 30px;">No notifications</p>');    end    else    begin      for Notif in FNotifications do      begin        var Color := GetTypeColor(Notif.NotifType);        var Icon := GetTypeIcon(Notif.NotifType);        var Opacity: string;        if Notif.IsRead then          Opacity := '0.5'        else          Opacity := '1.0';โ€‹        HTML.AppendFormat(          '<div style="border-left: 4px solid %s; padding: 10px 15px; margin: 8px 0; ' +          '  background: white; border-radius: 0 4px 4px 0; opacity: %s; ' +          '  box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer;" ' +          '  onclick="Notif:MarkAsRead(''%s'')">' +          '  <div style="display: flex; justify-content: space-between;">' +          '    <strong>%s %s</strong>' +          '    <small style="color: #999;">%s</small>' +          '  </div>' +          '  <p style="margin: 5px 0 0; color: #555;">%s</p>' +          '  %s' +          '</div>',          [Color, Opacity, Notif.ID, Icon, Notif.Title,           FormatDateTime('hh:nn:ss', Notif.Timestamp),           Notif.Message,           IfThen(not Notif.IsRead,             '<small style="color: ' + Color + ';">Click to mark as read</small>', '')]);      end;    end;โ€‹    HTML.AppendLine('</div>');    HTMLRender1.HTML.Text := HTML.ToString;  finally    HTML.Free;  end;end;โ€‹end.

Common Gotchas#

Connection lifecycle. The WebSocket connection runs on a background thread. Every event handler fires on that background thread. If you touch any UI control without TThread.Synchronize, your application will crash with access violations, or worse, silently corrupt UI state. Always wrap UI updates:

pascal
// Wrong -- will crashprocedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);begin  Label1.Text := AMessage;  // Access violation!end;โ€‹// Rightprocedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);begin  TThread.Synchronize(nil, procedure  begin    Label1.Text := AMessage;  end);end;

Thread safety with shared data. If your message handler writes to a TList or TDictionary that the UI thread also reads, you need synchronization. The simplest approach is to do everything inside TThread.Synchronize. For high-frequency messages, consider a thread-safe queue.

Reconnect re-subscription. When auto-reconnect opens a new connection, the server does not remember your subscriptions from the previous connection. Always re-subscribe in the OnConnected handler:

pascal
procedure TForm1.OnWSOpen(Sender: TObject);begin  TThread.Synchronize(nil, procedure  begin    // This runs on every connect, including reconnects    Tina4WebSocket1.Send('{"type": "subscribe", "channel": "updates"}');  end);end;

Memory management with TJSONObject. Every TJSONObject or TJSONArray you create from parsing must be freed. In the message handler, parse once, extract what you need, free immediately:

pascal
procedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);var  JSON: TJSONObject;begin  JSON := StrToJSONObject(AMessage);  if not Assigned(JSON) then Exit;  try    // Extract values here    var Name := JSON.GetValue<String>('name', '');    // Use the values...  finally    JSON.Free;  // Always free  end;end;

Disconnect before destroy. Always disconnect the WebSocket before the form is destroyed. If the background thread tries to fire an event after the form is freed, you get a use-after-free crash:

pascal
procedure TForm1.FormDestroy(Sender: TObject);begin  Tina4WebSocket1.AutoReconnect := False;  // Stop reconnection first  if Tina4WebSocket1.IsConnected then    Tina4WebSocket1.Disconnect;end;

Secure connections (WSS). For wss:// URLs, you need the same OpenSSL DLLs required for HTTPS REST calls. Without them, the connection will fail silently. See the Installation chapter for SSL setup details.