unit Myc.Signals; interface uses System.SysUtils, Myc.Core.Notifier; // Assuming Myc.Core.Notifier is available type IMycLatch = interface; // Forward declaration for the latch interface TMycSignal = class; // Base or related signal type IMycSubscriber = interface // Interface for an entity that can be notified by a signal or latch. // Returns true if further notifications are expected, false otherwise. function Notify: Boolean; end; TMycSubscription = record private FSignal: TMycSignal; // Could also be a more general IMycSignalProvider if TMycLatch isn't a TMycSignal FTag: TMycNotifyList.TTag; public class operator Initialize(out Dest: TMycSubscription); class operator Finalize(var Dest: TMycSubscription); class operator Assign(var Dest: TMycSubscription; const [ref] Src: TMycSubscription); // Setup associates the subscription with a signal and a specific tag from TMycNotifyList. procedure Setup(ASignal: TMycSignal; ATag: TMycNotifyList.TTag); inline; // Unsubscribe removes this subscription from the signal it's associated with. procedure Unsubscribe; end; IMycSignal = interface // Allows a subscriber to register for notifications. // Returns a subscription record that manages the lifetime of the subscription. function Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; end; TMycSignal = class abstract(TInterfacedObject, IMycSignal) public // Abstract method for subscribing to this signal. function Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; virtual; abstract; // Abstract method for unsubscribing using a tag. procedure Unsubscribe(Tag: TMycNotifyList.TTag); virtual; abstract; end; IMycState = interface(IMycSignal) // Represents a state that can be queried (IsSet) and subscribed to for changes. {$region 'property access'} function GetIsSet: Boolean; {$endregion} // IsSet is true if the state has been reached or the condition met. property IsSet: Boolean read GetIsSet; end; // IMycLatch represents a synchronization primitive that acts like a gate. // Once its internal condition is met (e.g., a count reaches zero), it becomes "set" // (its IMycState.IsSet becomes true) and remains set. // It can be notified (as an IMycSubscriber) to progress towards its "set" state. IMycLatch = interface(IMycSubscriber) {$region 'property access'} // Provides access to the IMycState interface of the latch, // which indicates if the latch is set and allows subscriptions to this state change. function GetState: IMycState; {$endregion} property State: IMycState read GetState; end; // TMycLatch implements a countdown latch. // It is initialized with a count. Calls to its Notify method (as an IMycSubscriber) // decrement this count. When the count reaches zero, the latch transitions to the "set" // state (IsSet becomes true), notifies its current subscribers once, and then remains set. TMycLatch = class(TMycSignal, IMycLatch, IMycState) strict private FSubscribers: TMycNotifyList; // List of subscribers waiting for this latch to be set. class var FNull: IMycLatch; class constructor ClassCreate; private [volatile] FCount: Integer; // The internal countdown value. function GetState: IMycState; function GetIsSet: Boolean; public // Creates the latch with an initial count. // If ACount is 0 or less, the latch is initially set. constructor Create(ACount: Integer); destructor Destroy; override; // Creates a new countdown latch initialized with the given count. class function CreateLatch(Count: Integer): IMycLatch; static; // IMycSignal implementation (from TMycSignal) function Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; override; procedure Unsubscribe(Tag: TMycNotifyList.TTag); override; // IMycSubscriber implementation for TMycLatch itself. // Decrements the internal count. If the count reaches zero, // registered subscribers are notified and the latch becomes permanently set. function Notify: Boolean; class property Null: IMycLatch read FNull; end; implementation uses System.SyncObjs; // For TInterlocked if used, though it wasn't in the original TMycSemaphore.Notify directly visible { TMycSubscription } procedure TMycSubscription.Setup(ASignal: TMycSignal; ATag: TMycNotifyList.TTag); begin Unsubscribe; // Ensure any previous subscription is cleared. FSignal := ASignal; FTag := ATag; end; procedure TMycSubscription.Unsubscribe; begin if FTag <> nil then // Check if currently subscribed begin Assert(Assigned(FSignal), 'FSignal is not assigned during Unsubscribe'); FSignal.Unsubscribe(FTag); FTag := nil; // Mark as unsubscribed end; end; class operator TMycSubscription.Assign(var Dest: TMycSubscription; const [ref] Src: TMycSubscription); begin // This default assignment might not be what's intended if Src is a temporary. // Proper management would involve Dest taking ownership or Src being managed. // However, if it's just copying the record fields: Dest.Setup(Src.FSignal, Src.FTag); // To prevent Src from unsubscribing if it's a temporary and Dest now "owns" the subscription, // one might consider nil'ing out Src.FTag, but that depends on usage patterns. // Given the `const [ref] Src`, this assignment implies Dest should reflect Src. // If Src is finalized later, it will call Unsubscribe. This might be problematic if Dest also expects to manage it. // A robust solution for record assignment with resource management often requires more careful design // or using interfaces/objects for the subscription itself. // For now, keeping it simple as per original structure. end; class operator TMycSubscription.Initialize(out Dest: TMycSubscription); begin Dest.FSignal := nil; // Ensure FSignal is initialized Dest.FTag := nil; // Initialize tag to nil, indicating no active subscription. end; class operator TMycSubscription.Finalize(var Dest: TMycSubscription); begin Dest.Unsubscribe; // Automatically unsubscribe when the record goes out of scope. end; { TMycLatch } constructor TMycLatch.Create(ACount: Integer); begin inherited Create; FCount := ACount; // Set the initial countdown value. FSubscribers.Create; // Initialize the list of subscribers. end; { TMycLatch } class constructor TMycLatch.ClassCreate; begin // Create1 a null latch instance that is initially (and always) set. FNull := TMycLatch.Create(0); end; destructor TMycLatch.Destroy; begin FSubscribers.Destroy; // Clean up the subscriber list. inherited; end; class function TMycLatch.CreateLatch(Count: Integer): IMycLatch; begin // Factory method to create a new TMycLatch instance. if Count > 0 then Result := TMycLatch.Create(Count) else Result := FNull; end; function TMycLatch.Notify: Boolean; var currentCountAfterDecrement: Integer; shouldNotifySubscribers: Boolean; begin FSubscribers.Lock; // Lock to protect FCount and FSubscribers modifications/iterations try currentCountAfterDecrement := TInterlocked.Decrement(FCount); // Defend against extreme underflow if Notify could be called excessively. // This helps maintain a semblance of sanity for FCount, though ideally // it shouldn't be needed if used as a typical countdown latch. if currentCountAfterDecrement < -MaxInt div 2 then TInterlocked.Increment(FCount); // Try to pull it back from extreme negative. // Notify subscribers only when the count transitions to zero. shouldNotifySubscribers := (currentCountAfterDecrement = 0); if shouldNotifySubscribers then begin FSubscribers.Notify( function(Subscriber: IMycSubscriber): Boolean begin // Notify each subscriber. The result of Subscriber.Notify indicates // if that subscriber expects further notifications (which, for a latch, is typically false after the first one). Result := Subscriber.Notify; end); end; // A latch, once set (count <= 0), remains set. // This Notify method itself returns true if the count is still > 0, // indicating the latch has not yet been "set" by this Notify call. Result := currentCountAfterDecrement > 0; // If the latch has become set (or was already set and Notify is called again making count more negative), // unadvise all subscribers. This makes it a one-shot notification for this set of subscribers. if currentCountAfterDecrement <= 0 then FSubscribers.UnadviseAll; finally FSubscribers.Release; end; end; function TMycLatch.GetIsSet: Boolean; begin // The latch is considered "set" if its count has reached zero or less. Result := FCount <= 0; end; function TMycLatch.GetState: IMycState; begin // TMycLatch itself implements IMycState. exit(Self); end; function TMycLatch.Subscribe(const Subscriber: IMycSubscriber): TMycSubscription; var alreadySet: Boolean; begin if not Assigned(Subscriber) then begin Result.Setup(nil, nil); // Return an empty/invalid subscription exit; end; // It's good practice to manage ref counts for interfaces passed around, // especially if their lifetime is tied to the subscription. // TMycNotifyList should handle AddRef/Release for stored subscribers if it holds interfaces. // If direct Advise/Unadvise handles it, this explicit AddRef/Release pair might be for safety // during the decision-making part of this method. Subscriber._AddRef; // Manually increment ref count for safety during this method's logic try FSubscribers.Lock; try // Check if the latch is already set. // This uses FCount directly rather than GetIsSet to avoid re-entrancy issues if GetIsSet had complex logic. alreadySet := (FCount <= 0); if alreadySet then begin // If already set, notify the new subscriber immediately and do not add to list. // The subscription will be effectively null/empty as it's a one-shot notification. Result.Setup(nil, nil); // No persistent subscription needed. Subscriber.Notify; // Notify immediately. end else begin // If not yet set, advise the subscriber to the list. // The TMycSubscription record will hold the tag for later unsubscription. Result.Setup(Self, FSubscribers.Advise(Subscriber)); end; finally FSubscribers.Release; end; finally Subscriber._Release; // Release the ref count taken at the beginning of the method. end; end; procedure TMycLatch.Unsubscribe(Tag: TMycNotifyList.TTag); begin if Tag = nil then // No valid tag to unadvise. exit; FSubscribers.Lock; try FSubscribers.Unadvise(Tag); // Remove the subscriber associated with this tag. finally FSubscribers.Release; end; end; end.