2023-01-02 22:24:03 +01:00
|
|
|
// *************************************************************************** }
|
|
|
|
//
|
|
|
|
// Delphi MVC Framework
|
|
|
|
//
|
2024-01-02 17:04:27 +01:00
|
|
|
// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team
|
2023-01-02 22:24:03 +01:00
|
|
|
//
|
|
|
|
// https://github.com/danieleteti/delphimvcframework
|
|
|
|
//
|
|
|
|
// ***************************************************************************
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
// ***************************************************************************
|
|
|
|
|
|
|
|
unit MVCFramework.SSEController;
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
uses
|
|
|
|
System.SysUtils,
|
|
|
|
MVCFramework,
|
|
|
|
MVCFramework.Commons,
|
|
|
|
System.Generics.Collections;
|
|
|
|
|
|
|
|
type
|
|
|
|
TMVCSSEDefaults = class sealed
|
2023-01-06 15:18:50 +01:00
|
|
|
public const
|
|
|
|
/// <summary>
|
|
|
|
/// Charset of SSE messages encoding
|
|
|
|
/// </summary>
|
|
|
|
SSE_CONTENT_CHARSET = TMVCConstants.DEFAULT_CONTENT_CHARSET;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Force client to reconnect again after specified milliseconds
|
|
|
|
/// </summary>
|
|
|
|
SSE_RETRY_TIMEOUT = 10000;
|
2023-01-02 22:24:03 +01:00
|
|
|
end;
|
|
|
|
|
|
|
|
TSSEMessage = record
|
|
|
|
Event: string;
|
|
|
|
Data: string;
|
|
|
|
Id: String;
|
|
|
|
end;
|
|
|
|
|
|
|
|
TMVCSSEMessages = TArray<TSSEMessage>;
|
|
|
|
|
|
|
|
TMVCSSEController = class abstract(TMVCController)
|
|
|
|
protected
|
|
|
|
fSSECharset: string;
|
2023-01-06 15:18:50 +01:00
|
|
|
fRetryTimeout: UInt32;
|
2023-01-02 22:24:03 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Overwrite this method in inherited class !
|
|
|
|
/// </summary>
|
|
|
|
function GetServerSentEvents(const LastEventID: String): TMVCSSEMessages; virtual; abstract;
|
|
|
|
public
|
2023-01-06 15:18:50 +01:00
|
|
|
constructor Create(
|
2023-01-17 08:52:26 +01:00
|
|
|
const ASSECharset: string;
|
|
|
|
const ARetryTimeout: UInt32); reintroduce; overload;
|
2023-01-17 08:30:22 +01:00
|
|
|
constructor Create; overload; override;
|
2023-01-02 22:24:03 +01:00
|
|
|
[MVCPath]
|
|
|
|
[MVCHTTPMethod([httpGET])]
|
|
|
|
[MVCProduces('text/event-stream')]
|
|
|
|
procedure Index;
|
|
|
|
end;
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
uses
|
2023-01-14 17:18:32 +01:00
|
|
|
IdContext, IdHTTPWebBrokerBridge, IdIOHandler, idGlobal;
|
2023-01-02 22:24:03 +01:00
|
|
|
|
2023-01-06 15:18:50 +01:00
|
|
|
constructor TMVCSSEController.Create(
|
2023-01-17 08:52:26 +01:00
|
|
|
const ASSECharset: string;
|
|
|
|
const ARetryTimeout: UInt32);
|
2023-01-02 22:24:03 +01:00
|
|
|
begin
|
|
|
|
inherited Create;
|
|
|
|
fSSECharset := ASSECharset;
|
2023-01-06 15:18:50 +01:00
|
|
|
fRetryTimeout := ARetryTimeout;
|
2023-01-02 22:24:03 +01:00
|
|
|
end;
|
|
|
|
|
|
|
|
type
|
|
|
|
TIdHTTPAppResponseAccess = class(TIdHTTPAppResponse);
|
|
|
|
|
2023-01-17 08:30:22 +01:00
|
|
|
constructor TMVCSSEController.Create;
|
|
|
|
begin
|
2023-01-17 08:52:26 +01:00
|
|
|
Create(TMVCSSEDefaults.SSE_CONTENT_CHARSET, TMVCSSEDefaults.SSE_RETRY_TIMEOUT);
|
2023-01-17 08:30:22 +01:00
|
|
|
end;
|
|
|
|
|
2023-01-02 22:24:03 +01:00
|
|
|
procedure TMVCSSEController.Index;
|
|
|
|
var
|
|
|
|
lRawContext: TIdContext;
|
|
|
|
lDataList: TMVCSSEMessages;
|
|
|
|
lSSEData: TSSEMessage;
|
|
|
|
lLastEventID: String;
|
|
|
|
lIOHandler: TIdIOHandler;
|
|
|
|
const
|
|
|
|
EOL = #13#10;
|
|
|
|
begin
|
|
|
|
inherited;
|
2023-01-06 15:07:44 +01:00
|
|
|
if not (Context.Response.RawWebResponse is TIdHTTPAppResponse) then
|
|
|
|
begin
|
|
|
|
raise EMVCException.Create(HTTP_STATUS.InternalServerError, ClassName + ' can only be used with INDY based application server');
|
|
|
|
end;
|
|
|
|
|
2023-01-02 22:24:03 +01:00
|
|
|
lRawContext := TIdHTTPAppResponseAccess(Context.Response.RawWebResponse).FThread;
|
|
|
|
|
2023-01-06 15:07:44 +01:00
|
|
|
lLastEventID := Context.Request.Headers[TMVCConstants.SSE_LAST_EVENT_ID].Trim;
|
|
|
|
|
2023-01-02 22:24:03 +01:00
|
|
|
lIOHandler := lRawContext.Connection.IOHandler;
|
|
|
|
lIOHandler.WriteBufferOpen();
|
|
|
|
lIOHandler.WriteLn('HTTP/1.1 200 OK');
|
|
|
|
lIOHandler.WriteLn(Format('Content-Type: text/event-stream; charset=%s', [fSSECharset]));
|
|
|
|
lIOHandler.WriteLn('Cache-Control: no-cache');
|
|
|
|
lIOHandler.WriteLn('Connection: keep-alive');
|
|
|
|
|
|
|
|
{TODO -oDanieleT -cSSE : We must handle CORS using constructor parameters}
|
|
|
|
lIOHandler.WriteLn('Access-Control-Allow-Origin: *');
|
|
|
|
lIOHandler.WriteLn('Access-Control-Allow-Methods: POST, PUT, DELETE, GET, OPTIONS');
|
|
|
|
lIOHandler.WriteLn('Access-Control-Request-Method: *');
|
|
|
|
lIOHandler.WriteLn('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
|
|
|
|
|
|
lIOHandler.WriteLn;
|
|
|
|
lIOHandler.WriteBufferClose;
|
|
|
|
|
|
|
|
while lRawContext.Connection.Connected do
|
|
|
|
begin
|
|
|
|
lDataList := [];
|
|
|
|
// query for next data list
|
|
|
|
lDataList := GetServerSentEvents(lLastEventID);
|
|
|
|
|
|
|
|
if (Length(lDataList) > 0) then
|
|
|
|
begin
|
|
|
|
lIOHandler.WriteBufferOpen;
|
|
|
|
for lSSEData in lDataList do
|
|
|
|
begin
|
|
|
|
if not lSSEData.Id.IsEmpty then
|
|
|
|
begin
|
|
|
|
lIOHandler.Write(Format('id: %s' + EOL, [lSSEData.Id]));
|
|
|
|
lLastEventID := lSSEData.Id;
|
|
|
|
end;
|
|
|
|
if not lSSEData.Event.IsEmpty then
|
|
|
|
begin
|
2023-01-14 17:18:32 +01:00
|
|
|
lIOHandler.Write(Format('event: %s' + EOL, [lSSEData.Event]), IndyTextEncoding(fSSECharset));
|
2023-01-02 22:24:03 +01:00
|
|
|
end;
|
2023-01-14 17:18:32 +01:00
|
|
|
lIOHandler.Write(Format('data: %s' + EOL, [lSSEData.Data]), IndyTextEncoding(fSSECharset));
|
2023-01-06 15:18:50 +01:00
|
|
|
lIOHandler.Write(Format('retry: %d' + EOL + EOL { end of message } , [FRetryTimeout]));
|
2023-01-02 22:24:03 +01:00
|
|
|
end;
|
|
|
|
lIOHandler.WriteBufferClose;
|
|
|
|
end;
|
|
|
|
Sleep(200); //arbitrary... some better approches?
|
|
|
|
end;
|
2023-01-06 15:07:44 +01:00
|
|
|
lRawContext.Connection.Disconnect;
|
2023-01-02 22:24:03 +01:00
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|