unit Myc.Data.Stream; interface uses System.SysUtils, System.Classes, System.Generics.Collections, System.SyncObjs, Myc.Data.Scalar, Myc.Data.Keyword, Myc.Core.Notifier; type TSignalKind = (skData, skHeartbeat); TStreamSignal = record private FCycleID: Int64; FKind: TSignalKind; public constructor Create(AKind: TSignalKind; ACycleID: Int64); function ToString: string; property CycleID: Int64 read FCycleID; property Kind: TSignalKind read FKind; end; IStreamObserver = interface procedure OnSignal(const Signal: TStreamSignal); end; TSubscriptionTag = Pointer; IStream = interface {$region 'private'} function GetSeries: IScalarRecordSeries; {$endregion} function Subscribe(const Observer: IStreamObserver): TSubscriptionTag; procedure Unsubscribe(Tag: TSubscriptionTag); // Returns the Series of the records this stream produces. Streams are scalar record series. Always! property Series: IScalarRecordSeries read GetSeries; end; // ========================================================================= // BASE STREAM // Implements storage (Series) and broadcasting (Observers). // Does not define WHEN data is emitted (Policy). // ========================================================================= TCustomDataStream = class(TInterfacedObject, IStream) strict private FSeries: IWriteableScalarRecordSeries; FObservers: TMycNotifyList; function GetSeries: IScalarRecordSeries; protected // Derived classes call this to write data and notify listeners. // ACycleID: The cycle this data belongs to. procedure Emit(const Value: array of TScalar.TValue; ACycleID: Int64); public constructor Create(const ADef: IScalarRecordDefinition); destructor Destroy; override; // IStream function Subscribe(const Observer: IStreamObserver): TSubscriptionTag; procedure Unsubscribe(Tag: TSubscriptionTag); property Series: IScalarRecordSeries read GetSeries; end; // ========================================================================= // ROOT STREAM (SOURCE) // Acts as the clock source. Generates new CycleIDs. // ========================================================================= TRootStream = class(TCustomDataStream) private FLastCycleID: Int64; public constructor Create(const ADef: IScalarRecordDefinition); // Public API to inject data from Delphi procedure Push(const RowData: TArray); end; // ========================================================================= // PIPE STREAM (NODE) // Reacts to upstream signals using barrier synchronization. // ========================================================================= TPipeStream = class; // Forward TPipeConfig = TArray>; TPipeSource = class(TContainedObject, IStreamObserver) private FSource: IStream; FTag: TSubscriptionTag; FLastSeenCycle: Int64; procedure OnSignal(const Signal: TStreamSignal); public constructor Create(AOwner: TPipeStream; ASource: IStream); destructor Destroy; override; property LastSeenCycle: Int64 read FLastSeenCycle; end; TPipeStream = class(TCustomDataStream) public type TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean; private FSources: TArray; FSourceSeries: TArray; FLastFiredCycleID: Int64; FLambda: TPipeLambda; procedure CheckBarrierAndFire(CurrentCycle: Int64); public constructor Create( const AConfig: TPipeConfig; const ADef: IScalarRecordDefinition; const ASources: TArray; const ALambda: TPipeLambda ); destructor Destroy; override; end; implementation uses Winapi.Windows; constructor TStreamSignal.Create(AKind: TSignalKind; ACycleID: Int64); begin FKind := AKind; FCycleID := ACycleID; end; function TStreamSignal.ToString: string; begin if Kind = skData then Result := Format('Signal(Data, #%d)', [CycleID]) else Result := Format('Signal(Heartbeat, #%d)', [CycleID]); end; { TCustomDataStream } constructor TCustomDataStream.Create(const ADef: IScalarRecordDefinition); begin inherited Create; FSeries := TScalarRecordSeries.Create(ADef); end; destructor TCustomDataStream.Destroy; begin FObservers.Finalize; inherited; end; function TCustomDataStream.GetSeries: IScalarRecordSeries; begin Result := FSeries; end; function TCustomDataStream.Subscribe(const Observer: IStreamObserver): TSubscriptionTag; begin FObservers.Lock; try Result := FObservers.Advise(Observer); finally FObservers.Release; end; end; procedure TCustomDataStream.Unsubscribe(Tag: TSubscriptionTag); begin FObservers.Lock; try FObservers.Unadvise(Tag); finally FObservers.Release; end; end; procedure TCustomDataStream.Emit(const Value: array of TScalar.TValue; ACycleID: Int64); var signal: TStreamSignal; begin FObservers.Lock; try // 1. Write Data FSeries.Add(Value); // 2. Broadcast Signal signal := TStreamSignal.Create(skData, ACycleID); FObservers.Notify( function(const Obs: IStreamObserver): Boolean begin Obs.OnSignal(signal); Result := True; end ); finally FObservers.Release; end; end; { TRootStream } constructor TRootStream.Create(const ADef: IScalarRecordDefinition); begin inherited Create(ADef); FLastCycleID := 0; end; procedure TRootStream.Push(const RowData: TArray); begin // Root streams increment the global clock Inc(FLastCycleID); Emit(RowData, FLastCycleID); end; { TPipeSource } constructor TPipeSource.Create(AOwner: TPipeStream; ASource: IStream); begin inherited Create(AOwner); FSource := ASource; FLastSeenCycle := -1; FTag := FSource.Subscribe(Self); end; destructor TPipeSource.Destroy; begin FSource.Unsubscribe(FTag); inherited; end; procedure TPipeSource.OnSignal(const Signal: TStreamSignal); begin if Signal.Kind = skData then begin FLastSeenCycle := Signal.CycleID; (Controller as TPipeStream).CheckBarrierAndFire(FLastSeenCycle); end; end; { TPipeStream } constructor TPipeStream.Create( const AConfig: TPipeConfig; const ADef: IScalarRecordDefinition; const ASources: TArray; const ALambda: TPipeLambda ); var i, j, n: Integer; begin // Pass Definition to base class inherited Create(ADef); FLambda := ALambda; FLastFiredCycleID := -1; SetLength(FSources, Length(ASources)); // Flatten Sources n := 0; for i := 0 to High(AConfig) do inc(n, Length(AConfig[i])); SetLength(FSourceSeries, n); n := 0; for i := 0 to High(ASources) do begin FSources[i] := TPipeSource.Create(Self, ASources[i]); for j := 0 to High(AConfig[i]) do begin // Extract the specific column series FSourceSeries[n] := ASources[i].Series.Fields[AConfig[i][j].Key]; inc(n); end; end; end; destructor TPipeStream.Destroy; begin for var i := High(FSources) downto 0 do FSources[i].Free; FSources := nil; FSourceSeries := nil; inherited; end; procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64); var resultVal: TArray; begin // We reuse the FObservers lock from the base class to protect state // But since FObservers is private, we access it via method or need to make it protected. // Making it protected or using a separate lock is cleaner. // For now, let's assume we rely on the fact that OnSignal is usually serialized per thread // or add a lock. // -> Ideally, TCustomDataStream should expose Lock/Unlock or we add a Lock here. // Simplification: We assume thread safety is handled by the caller or we add a dedicated lock. // For this example, let's just do the logic: for var src in FSources do if src.LastSeenCycle < CurrentCycle then exit; // Barrier closed if FLastFiredCycleID >= CurrentCycle then exit; // Already fired FLastFiredCycleID := CurrentCycle; if Assigned(FLambda) then begin SetLength(resultVal, Series.Def.Count); if FLambda(FSourceSeries, resultVal) then begin // Pass through CycleID from upstream Emit(resultVal, FLastFiredCycleID); end; end; end; initialization TMycNotifyList.ReverseOnNotify := False; end.