///|
/// An explicitly granted session owns its resources until disposal. Acquisition
/// must clean up partial resources itself if it raises before returning a session.
/// This host contract does not implement authentication or process transport.
pub(all) struct MetadataSession {
capabilities : @linked_metadata.Capabilities
dispose : () -> Unit raise
}
///|
priv enum MetadataSessionState {
Idle
Executing(Array[() -> Unit raise])
Disposing
CleanupFailed(String, Array[() -> Unit raise])
}
///|
/// Opaque host-owned lifecycle state. Failed cleanup retains its callbacks and
/// refuses further execution until an explicit cleanup retry succeeds.
pub struct MetadataSessions {
priv mut state : MetadataSessionState
}
///|
priv enum MetadataSessionSlotState {
Empty
Acquiring
Acquired(MetadataSession)
}
///|
priv struct MetadataSessionSlot {
mut state : MetadataSessionSlotState
}
///|
priv enum MetadataDisposal {
FinishExecution
RetryCleanup
}
///|
fn MetadataSessions::new() -> MetadataSessions {
{ state: Idle, }
}
///|
fn MetadataSessions::begin(self : MetadataSessions) -> Unit raise {
match self.state {
Idle => self.state = Executing([])
CleanupFailed(message, _) => raise @config.Invalid(message)
Executing(_) | Disposing =>
raise @config.Invalid(
"metadata.restore_session_active: Host execution is already active",
)
}
}
///|
fn MetadataSessions::acquire(
self : MetadataSessions,
slot : MetadataSessionSlot,
scope : @engine.ProjectMetadataScope,
acquire : (@engine.ProjectMetadataScope) -> MetadataSession raise,
) -> MetadataSession raise {
guard self.state is Executing(cleanups) else {
raise @linked_metadata.Invalid(
"metadata.restore_session_required",
scope.project_root,
"Execute this capability through its owning host",
)
}
match slot.state {
Acquired(session) => return session
Acquiring =>
raise @linked_metadata.Invalid(
"metadata.restore_session_active",
scope.project_root,
"Recursive session acquisition is unavailable",
)
Empty => ()
}
slot.state = Acquiring
let session = {
errdefer {
slot.state = Empty
}
acquire(scope) catch {
@linked_metadata.Invalid(_, _, _) as error => raise error
error =>
raise @linked_metadata.Invalid(
"metadata.restore_acquisition_failed",
scope.project_root,
error_message(error),
)
}
}
slot.state = Acquired(session)
cleanups.push(fn() raise {
// Revoke capability access even when disposal fails. Keep the cleanup
// callback itself in the failed owner so it can be retried explicitly.
slot.state = Empty
(session.dispose)()
})
session
}
///|
fn MetadataSessions::dispose(
self : MetadataSessions,
operation : MetadataDisposal,
) -> String? raise {
let cleanups = match self.state {
Idle => return None
Executing(cleanups) if operation is FinishExecution => cleanups
CleanupFailed(_, cleanups) if operation is RetryCleanup => cleanups
_ =>
raise @config.Invalid(
"metadata.restore_session_active: Cannot retry cleanup during execution",
)
}
self.state = Disposing
let failed = []
let errors : Array[String] = []
for i = cleanups.length() - 1; i >= 0; i = i - 1 {
let cleanup = cleanups[i]
cleanup() catch {
error => {
failed.push(cleanup)
errors.push(error_message(error))
}
}
}
if errors.is_empty() {
self.state = Idle
None
} else {
let message = "metadata.restore_cleanup_failed: " + errors.join("; ")
// Retain acquisition order: every disposal pass traverses it in reverse.
self.state = CleanupFailed(message, failed.rev())
Some(message)
}
}
///|
/// Bind project-specific capabilities without acquiring resources during
/// planning. The supplied acquisition callback runs lazily during run_content.
/// Its session is reused only within that execution and disposed before any
/// output publication. The callback must enforce its own explicit host grants.
/// Static engine capabilities and this binder remain mutually exclusive.
pub fn Host::register_project_metadata_provider(
self : Host,
engine : @engine.Engine,
acquire : (@engine.ProjectMetadataScope) -> MetadataSession raise,
) -> Unit raise {
engine.register_project_metadata_provider(scope => {
self.metadata_capabilities(scope, acquire)
})
}
///|
fn Host::metadata_capabilities(
self : Host,
scope : @engine.ProjectMetadataScope,
acquire : (@engine.ProjectMetadataScope) -> MetadataSession raise,
) -> @linked_metadata.Capabilities {
let slot = MetadataSessionSlot::{ state: Empty, }
let sessions = self.metadata_sessions
{
restore_library: Some(fn(predicate) raise {
let capabilities = sessions.acquire(slot, scope, acquire).capabilities
guard capabilities.restore_library is Some(restore) else {
raise @linked_metadata.Invalid(
"metadata.provider_unavailable", predicate, "The scoped session does not provide authenticated restoration",
)
}
if !capabilities.target_names_interpreter {
raise @linked_metadata.Invalid(
"metadata.required_interpreter_unavailable", predicate, "The scoped session does not grant the target-name interpreter",
)
}
restore(predicate)
}),
load_context: Some(fn(url, base) raise {
let capabilities = sessions.acquire(slot, scope, acquire).capabilities
guard capabilities.load_context is Some(load) else {
raise @linked_metadata.Invalid(
"metadata.context_resource_unavailable", url, "The scoped session does not provide this context",
)
}
load(url, base)
}),
target_names_interpreter: true,
provider_observer: {
notify: fn(signal) raise {
// Observation never acquires a session or recovers disposed authority.
if slot.state is Acquired(session) {
(session.capabilities.provider_observer.notify)(signal)
}
},
},
}
}
///|
/// A CLI selection of none does not request a service. Explicit embedding
/// capabilities keep their separate authority through the general binder.
fn Host::register_requested_metadata_provider(
self : Host,
engine : @engine.Engine,
acquire : (@engine.ProjectMetadataScope) -> MetadataSession raise,
) -> Unit raise {
engine.register_project_metadata_provider(scope => {
if scope.request is Disabled {
@linked_metadata.Capabilities::unavailable()
} else {
self.metadata_capabilities(scope, acquire)
}
})
}
///|
/// Retry only callbacks retained after failed cleanup. This never restores a
/// provider or permits a concurrent retry during an active host execution.
pub fn Host::dispose_metadata_sessions(self : Host) -> Unit raise {
if self.metadata_sessions.dispose(RetryCleanup) is Some(message) {
raise @config.Invalid(message)
}
}