Wait for a bounded operation group.
A scoped operation starts without suspending its owner. The owner can start several operations and wait on one bounded Completion_Set. Additive overloads cover these providers:
- Descriptor readiness, monotonic timers, stream and datagram sockets, and Internet or Unix-stream connection attempts and accepts.
- Managed connections,
Unique_Bufferstreams, high-level connection data, and standalone TLS. - Completion-driven positional files, file-watcher
Next, and retained task-resultWait. Flyology.Channels.Boundedsend and receive operations, plusFlyology.Buffers.Channelsownership transfers.
These operations create no helper task, per-operation task stack, callback thread, or steady-state heap allocation. A successful accept or managed connect owns its new socket until Finish transfers it, so abandoning the operation closes that socket. Scoped file operations currently require a lightweight owner; the existing synchronous file procedures still work in both lanes.
declare
-- Four root operations plus one gate need five bounded slots.
Set : aliased Flyology.Operations.Completion_Set (5);
-- Supplying Set selects the additive operation-producing overload.
-- Without Set, these familiar calls keep their synchronous behavior.
-- Each declaration below starts its operation without parking this task.
Alarm : aliased Flyology.IO.Timers.Timer_Operation :=
Flyology.IO.Timers.Sleep_For
(Set => Set'Access, Interval => 0.050);
Incoming : aliased Flyology.IO.Sockets.Receive_Operation :=
Flyology.IO.Sockets.Receive
(Set => Set'Access,
Socket => Peer'Access,
Item => Network_Buffer'Access,
Timeout => 1.0);
Loading : aliased Flyology.IO.Files.Read_Operation :=
Flyology.IO.Files.Read_At
(Set => Set'Access,
File => Input_File,
Offset => 0,
Item => Read_Buffer'Access);
Saving : aliased Flyology.IO.Files.Write_Operation :=
Flyology.IO.Files.Write_At
(Set => Set'Access,
File => Output_File,
Offset => 0,
Item => Write_Buffer'Access);
-- A gate is itself an operation and occupies the fifth slot.
All_Done : Flyology.Operations.Gate_Operation :=
Flyology.Operations.Wait_All
(Set => Set'Access,
Members =>
[Flyology.Operations.Reference (Alarm),
Flyology.Operations.Reference (Incoming),
Flyology.Operations.Reference (Loading),
Flyology.Operations.Reference (Saving)]);
Completed : Flyology.Operations.Completion_Batch (Set.Capacity);
Received_Last, Read_Last, Written_Last :
Ada.Streams.Stream_Element_Offset;
begin
-- Wait_Some parks this task and drives ready operations in batches.
while Flyology.Operations.Is_Active (All_Done) loop
Flyology.Operations.Wait_Some (Set, Completed);
end loop;
-- Finish consumes each retained result, publishes out values or raises
-- its provider exception, and releases that operation's set slot.
Flyology.Operations.Finish (All_Done, Completed);
Flyology.IO.Timers.Finish (Alarm);
Flyology.IO.Sockets.Finish (Incoming, Received_Last);
Flyology.IO.Files.Finish (Loading, Read_Last);
Flyology.IO.Files.Finish (Saving, Written_Last);
end;
Without the leading Set, those same names keep their existing synchronous behavior. With a set, they eagerly return typed limited operations and retain their ordinary result or exception until the matching provider Finish. The example assumes the sockets, files, and aliased buffers were declared by the surrounding scope; nonempty scoped file operations currently require a lightweight owner.
The Unique_Buffer file overloads use explicit ownership transfer instead of borrowing an aliased array. Starting Read_At or Write_At moves the pool token into the operation and leaves the caller's handle vacant. Typed Finish returns the token after kernel ownership ends, even if Finish then raises a retained timeout, cancellation, or I/O error. Abandoning the operation drains first and releases the token to the pool. The pool must outlive the operation. On Darwin, cancellation or expiry of a submitted scoped POSIX AIO request waits for its natural completion notification before Flyology returns the token.
The file-watcher Next overload returns the same event and timeout outcome through typed Finish. It borrows the serialized watcher until terminal completion. Its scoped form leaves interrupt descriptors out deliberately: start each interrupt as an ordinary descriptor-readiness operation and combine it with the watcher through a gate. Recursive Next composes that ordinary watcher operation as a hidden child. The visible parent succeeds only after directory reconciliation finishes. Parent and child use two set slots, and an observing gate uses a third. Reconciliation performs bounded caller-lane metadata work after readiness and can occupy a lightweight event loop on a slow filesystem.
The high-level connection Receive, Receive_Exactly, Send_All, and Connections.TLS.Upgrade overloads acquire their generation-checked connection lease without blocking the owner. Upgrade composes every handshake request for read or write readiness; the data operations drive either plaintext or the installed TLS transport. A terminal result releases the lease before typed Finish, so another queued operation can use the connection while the first result remains retained. Before the owner task calls synchronous Close, finish each pending scoped operation or cancel and drain it. Close can wait for registered work, but only the owner can drive the pending operation. A close from another task is observed through the operation's close wake source.
The standalone Flyology.IO.TLS.Connection has the same additive pattern for Handshake, Receive, Receive_Exactly, Send_All, and Shutdown. One deadline spans lease acquisition and every provider retry, including cross-direction WANT_READ and WANT_WRITE. Typed Finish retains provider failures and receive bounds. Before the owner task calls synchronous TLS.Close, finish each pending operation or cancel and drain it. A close from another task wakes and drains the operation.
Flyology.Task_Results.Wait likewise accepts a completion set before either a task identity or an attached monitor. The operation retains the exact result sidecar during initiation, so the task object or source monitor may then leave scope; typed Finish returns the familiar Task_Observation. Several operations can subscribe to the same task completion and compose through ordinary gates without polling or extra tasks.
An instance of Flyology.Channels.Bounded keeps its familiar protected Send and Receive entries and adds same-name overloads that accept a completion set. A scoped send copies its value into the operation, so the source actual may leave scope. A scoped receive retains the dequeued value until typed Finish copies it out. Pending operations subscribe with caller-owned nodes and recheck the channel before parking, so applications do not call or see a separate arm protocol.
declare
Set : aliased Flyology.Operations.Completion_Set (3);
Message : aliased Messages.Receive_Operation :=
Messages.Receive (Set'Access, Inbox'Access);
Alarm : aliased Flyology.IO.Timers.Timer_Operation :=
Flyology.IO.Timers.Sleep_For (Set'Access, 0.050);
First : Flyology.Operations.Gate_Operation :=
Flyology.Operations.Wait_For_Success
(Set'Access,
[Flyology.Operations.Reference (Message),
Flyology.Operations.Reference (Alarm)]);
Batch : Flyology.Operations.Completion_Batch (Set.Capacity);
Value : Message_Type;
begin
while Flyology.Operations.Is_Active (First) loop
Flyology.Operations.Wait_Some (Set, Batch);
end loop;
Flyology.Operations.Finish (First, Batch);
if Flyology.Operations.Is_Terminal (Message) then
Messages.Finish (Message, Value);
else
Flyology.Operations.Cancel (Message);
begin
Messages.Finish (Message, Value);
exception
when Flyology.Operations.Operation_Cancelled => null;
end;
end if;
if Flyology.Operations.Is_Active (Alarm) then
Flyology.Operations.Cancel (Alarm);
end if;
begin
Flyology.IO.Timers.Finish (Alarm);
exception
when Flyology.Operations.Operation_Cancelled => null;
end;
end;
Flyology.Buffers.Channels applies the same protocol to single-owner buffers. Starting a scoped Send_Move moves the pool token into the operation and immediately leaves the source handle vacant. On success the channel keeps ownership and typed Finish leaves that handle vacant; on timeout, close, cancellation, or driver failure, Finish first restores the original buffer and then raises. A scoped Receive_Move does not borrow a destination while it waits. The completed operation owns the dequeued buffer until typed Finish moves it into a vacant same-pool handle.
Own buffers while a receive races a timer
Channel is a controlled tagged type, so its same-package operation functions accept a named Channel_Access capability. For a local aliased channel, Unchecked_Access is valid only because this scope finishes or finalizes every operation before the channel and pool leave scope.
declare
Pool : aliased Flyology.Buffers.Pool
(Block_Size => 4_096, Capacity => 4);
Queue : aliased Flyology.Buffers.Channels.Channel
(Owner => Pool'Access, Capacity => 2);
Set : aliased Flyology.Operations.Completion_Set (3);
Outgoing, Incoming : Flyology.Buffers.Unique_Buffer (Pool'Access);
Message : aliased Flyology.Buffers.Channels.Receive_Operation :=
Flyology.Buffers.Channels.Receive_Move
(Set'Access, Queue'Unchecked_Access, Timeout => 1.0);
Alarm : aliased Flyology.IO.Timers.Timer_Operation :=
Flyology.IO.Timers.Sleep_For (Set'Access, 0.050);
First : Flyology.Operations.Gate_Operation :=
Flyology.Operations.Wait_For_Success
(Set'Access,
[Flyology.Operations.Reference (Message),
Flyology.Operations.Reference (Alarm)]);
Batch : Flyology.Operations.Completion_Batch (Set.Capacity);
begin
Flyology.Buffers.Acquire (Outgoing);
Flyology.Buffers.Channels.Send_Move (Queue, Outgoing);
-- Outgoing is vacant; Message owns the buffer once it succeeds.
while Flyology.Operations.Is_Active (First) loop
Flyology.Operations.Wait_Some (Set, Batch);
end loop;
Flyology.Operations.Finish (First, Batch);
if Flyology.Operations.Is_Terminal (Message) then
Flyology.Buffers.Channels.Finish (Message, Incoming);
-- Incoming is now the sole owner.
Flyology.Buffers.Release (Incoming);
else
Flyology.Operations.Cancel (Message);
begin
Flyology.Buffers.Channels.Finish (Message, Incoming);
exception
when Flyology.Buffers.Channels.Operation_Cancelled => null;
end;
end if;
if Flyology.Operations.Is_Active (Alarm) then
Flyology.Operations.Cancel (Alarm);
end if;
begin
Flyology.IO.Timers.Finish (Alarm);
exception
when Flyology.Operations.Operation_Cancelled => null;
end;
end;
Wait_Some without a count waits for one newly completed operation and returns the complete observed batch. Its counted overload and Wait_At_Least accept a minimum completion count. Wait_All waits until no operation remains pending. Wait_For_Success waits for one success; Wait_For_Successes accepts a success count. The wait returns failures and cancellations, but they do not satisfy a success threshold. The wait returns when the threshold is met or becomes impossible.
Those wait names also have function overloads that produce a limited Gate_Operation. A gate consumes one completion-set slot and composes uniformly with provider operations or earlier gates: it appears in batches, has an outcome, and can be referenced by a later gate. Members are a fixed same-set generation snapshot, and their results remain retained until dependent gates terminalize. Cancelling a gate detaches the observer without cancelling its members.
Advanced: compose a two-success gate
This version turns a success threshold into a first-class operation. The set-level Wait_Some only parks the owner and drives completion batches until the gate publishes its own outcome.
declare
Set : aliased Flyology.Operations.Completion_Set (4);
Input : aliased Flyology.IO.Readiness_Operation :=
Flyology.IO.Wait
(Set => Set'Access,
FD => Input_FD,
Condition => Flyology.IO.For_Read);
Output : aliased Flyology.IO.Readiness_Operation :=
Flyology.IO.Wait
(Set => Set'Access,
FD => Output_FD,
Condition => Flyology.IO.For_Write);
Alarm : aliased Flyology.IO.Timers.Timer_Operation :=
Flyology.IO.Timers.Sleep_For
(Set => Set'Access, Interval => 0.050);
Quorum : Flyology.Operations.Gate_Operation :=
Flyology.Operations.Wait_For_Successes
(Set => Set'Access,
Members =>
[Flyology.Operations.Reference (Input),
Flyology.Operations.Reference (Output),
Flyology.Operations.Reference (Alarm)],
Required => 2);
Done : Flyology.Operations.Completion_Batch (Set.Capacity);
begin
while Flyology.Operations.Is_Active (Quorum) loop
Flyology.Operations.Wait_Some (Set, Done);
end loop;
Flyology.Operations.Finish (Quorum, Done);
-- Resolve or explicitly discard every root operation afterward.
end;
Normal code calls the provider-specific Finish exactly once for every started operation. It commits output values, reports the retained provider exception, and releases the slot. Gate Finish returns its stable member snapshot. Consume explicitly discards a terminal result. Scope finalization safely cancels, drains, discards, and releases an abandoned operation, but it is a safety net rather than the normal result path. Because Ada does not copy out an out parameter when a call raises, a gate batch is undefined when gate Finish raises Operation_Cancelled.
This is an additive overload of the familiar synchronous call. Flyology.IO.Wait (FD, Condition, Timeout) still waits and returns a Boolean; Flyology.IO.Wait (Set, FD, Condition) starts and returns a limited Readiness_Operation. The explicit set selects the bounded storage that owns the eager operation and gives later gates a stable member identity.
The other synchronous overloads remain available too. A pending operation borrows its referenced buffers and resources until terminal completion. Raw array and socket arguments use explicit access parameters so the long-lived borrow is visible at the call site. Unique-buffer socket operations retain the owning handle but enter its data callback only for an immediate nonblocking step. Receive_Datagram retains the complete address, truncation, and ECN metadata for its typed Finish; an empty buffer still consumes one zero-length packet. A submitted file array remains kernel-owned until completion; cancellation and scope exit drain it before the buffer or operation can leave scope.
The user API starts operations; the driver protocol advances them
An application initializes a typed limited operation from the additive function overload and waits on its completion set. It does not call Arm. A provider library calls Flyology.Operations.Drivers.Start and performs one bounded immediate step with its Drive primitive. The provider then arms readiness or a deadline, retains an external completion source, or publishes a terminal result. Cancellation is a separate provider primitive and must drain retained kernel input before publishing terminal state.
A zero-time operation deadline performs an immediate poll. The provider gets its first bounded step, and descriptor readiness observable in that same poll drives one step before the operation's deadline is classified. Unrelated expired operations still complete in the same snapshot. This keeps scoped socket and TLS behavior aligned with the familiar synchronous wait.
Higher-level providers compose the same public operation values; they do not access another provider's internal driver state. A composite operation owns typed child operations constrained by the same set, starts one through the provider's same-name in out overload, and calls Continue_After (Parent, Child). Readiness, relative and absolute timers, socket transfers, and positional file transfers all provide this form. The child uses a bounded slot and is driven normally, but user waits and gates see only the parent. When the child terminalizes, Flyology drives the parent with Dependency_Changed on the owner's task stack. The parent calls the child's typed Finish, calls Release so another child type can reuse that slot, and then starts its next child or completes. Cancellation propagates through the active child and drains it before the parent terminalizes. A request-send followed by response-receive therefore needs two slots total and no helper task, nested wait, callback, private I/O state, or second stack.
Typed children fit providers whose child type is known statically. A class-wide HTTP or database transport cannot embed a runtime-selected socket, connection, or TLS operation without allocation or type erasure. For that case, a concrete adapter stores one definite Flyology.IO.Connections.Drivers.Capability or Flyology.IO.TLS.Drivers.Capability that is independent of every completion set. The higher-level operation owns the only slot and asks the capability to start or poll its lease, perform one bounded transport step, and arm that same outer operation for the required source and shared deadline. Release returns connection ownership before the outer result becomes terminal. Descriptors, TLS sessions, and lease generations remain hidden, and no child operation, helper task, or allocation is introduced.
Choose the operation-producing form from its data flow
- Function result
- Use a function when one limited operation is the natural result. Initialize the result in place because a limited operation cannot be assigned later.
outoperation- Use an
outformal only when a procedure produces genuinely fresh caller-owned operation state or several results and never reads prior slot state. It is not a substitute for rearming a limited object whose completion-set discriminant is already fixed. in outoperation- Use an
in outformal only when initiation reads and mutates established request state. Explicit rearm and restart operations use this mode.
Keep the other formal modes tied to their data flow. Use in for immutable values and read-only borrowed buffers. Use in out for writable buffers or transferred ownership. Use out for values produced without reading their previous state.