(* The lifecycle manager decides when a registered object may leave memory. It works purely on the object tree and never touches storage: collection here means dropping the in-memory registry entry, not deleting anything on disk. Durable-backed objects (relations, multigroups, branch trees) can be rebuilt from their encoded form on the next access, so evicting them is safe and keeps memory bounded. Runtime-only objects (sessions, and later transactions) have no durable form, so their collection is a real teardown; the manager surfaces those to the caller as disposals rather than acting on storage itself. *) (* @uses Object -- find/update, unregister, reference_edges, has_children *) (* @needs Path *) (* @needs Condition *) type tree = Object.rnt_object_tree (* A runtime object that was collected and now needs an external teardown the lifecycle manager cannot perform on its own: closing a session, and later rolling back a transaction. The caller drains these after a collect. *) type disposal = {path: Concepts.Path.t; kind: Object.rnt_object_kind} let monitor (tree : tree) (path : Concepts.Path.t) : tree = Object.update path (fun r -> {r with Object.handle_count= r.Object.handle_count + 1}) tree (* Pin is an explicit keep-alive that bypasses collection. It does not touch reference_count: pinning is a decision, reference counting is bookkeeping of real edges. Keeping them separate is what stops the two from being mistaken for each other. *) let pin (tree : tree) (path : Concepts.Path.t) : tree = Object.update path (fun r -> {r with Object.pinned= true}) tree let unpin (tree : tree) (path : Concepts.Path.t) : tree = Object.update path (fun r -> {r with Object.pinned= false}) tree (* An object may be collected only when nothing holds it open, nothing depends on it, it has not been pinned, and its kind is not exempt (a branch). This works on the registry record directly so callers that already hold it (the cascade) need not walk the tree a second time. *) let registry_eligible (r : Object.registry) : bool = r.Object.handle_count = 0 && r.Object.reference_count = 0 && (not r.Object.pinned) && not (Object.is_gc_exempt r.Object.entry.Object.kind) let is_eligible_for_gc (tree : tree) (path : Concepts.Path.t) : bool = match Object.find path tree with Some r -> registry_eligible r | None -> false (* Contention reports whether a mutation must wait. An exclusive object with an open handle, such as a branch head being read, cannot be modified until the handle closes. This gates writers; it is not part of collection. *) let contention (tree : tree) (path : Concepts.Path.t) : bool = match Object.find path tree with | Some r -> r.Object.entry.Object.exclusive && r.Object.handle_count > 0 | None -> false (* Cascade the collection at path, threading the pruned tree and the disposals gathered so far. Disposals are appended (snoc) as each object is torn down, so the finger tree hands them back parent-first, left-to-right without any reversal pass. Order of teardown falls out of the counts. Collecting an object drops the reference_count of every input it depended on (Object.unregister undoes the edges), which can make those inputs eligible in turn, so we recurse into them. A populated namespace is kept, not collected and not an error: it is scaffolding for the objects still living under it. *) let rec cascade (tree : tree) (disposals : disposal BatFingerTree.t) (path : Concepts.Path.t) : (tree * disposal BatFingerTree.t, Concepts.Condition.condition) result = match Object.find path tree with | None -> Ok (tree, disposals) | Some r when not (registry_eligible r) -> Ok (tree, disposals) | Some r -> let kind = r.Object.entry.Object.kind in begin match kind with | Object.Namespace _ when Object.has_children path tree -> Ok (tree, disposals) | _ -> let edges = Object.reference_edges kind in let disposals = if Object.is_runtime kind then BatFingerTree.snoc disposals {path; kind} else disposals in Result.bind (Object.unregister path tree) (fun tree -> BatFingerTree.fold_left (fun acc dep -> Result.bind acc (fun (tree, disposals) -> cascade tree disposals dep) ) (Ok (tree, disposals)) edges ) end let try_collect (tree : tree) (path : Concepts.Path.t) : (tree * disposal BatFingerTree.t, Concepts.Condition.condition) result = cascade tree BatFingerTree.empty path (* Close a handle, then see whether the object can now go. *) let unmonitor (tree : tree) (path : Concepts.Path.t) : (tree * disposal BatFingerTree.t, Concepts.Condition.condition) result = let tree = Object.update path (fun r -> if r.Object.handle_count = 0 then r else {r with Object.handle_count= r.Object.handle_count - 1} ) tree in try_collect tree path (* Public entry point for an explicit collection request. *) let collect (tree : tree) (path : Concepts.Path.t) : (tree * disposal BatFingerTree.t, Concepts.Condition.condition) result = try_collect tree path (* Session reaping is not built here yet. A lost connection is detected by a per-session inactivity timer whose expiry drives collect on the session. Design and tasks are tracked in issue #24. *)