diff --git a/src/MeshWeaver.Documentation/Data/Architecture/WriteVerdictTotality.md b/src/MeshWeaver.Documentation/Data/Architecture/WriteVerdictTotality.md index bcb37af3c1..bfc75c1c74 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/WriteVerdictTotality.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/WriteVerdictTotality.md @@ -466,10 +466,16 @@ subscriber cannot fail to answer: 🚨 **None of this bounds a source that NEVER terminates.** `DetachedReplyOutcome.Of` is about the three terminations Rx *delivers*; the fourth outcome — never emits, never completes, the section -"The FOURTH outcome" above — reaches no arm at all, and no composition over the source can invent -one. A liveness bound stays the leg's own responsibility and is separately present where the -source can starve: `InnerCreateVerdictBound` on the inner create, `NodeOpForwardTimeout` inside the -no-op probe. Totality and liveness are different properties; this section is about the first. +"The FOURTH outcome" above — reaches no arm at all, and `Of` cannot rescue it, because every +operator it applies is **notification-driven**: `Take(1)` synthesises a completion but only *after +a value*, `Catch` substitutes a sequence but only *after a fault*, `DefaultIfEmpty` fires only *on +a completion*. From a source that has produced nothing, none of them can fire. The one +**clock**-driven operator is `Timeout`, which is why it — and only it — can terminate a silent +source; it is separately present where the source can starve: +`InnerCreateVerdictBound` on the inner create, `NodeOpForwardTimeout` inside the no-op probe. +Compose `Of` **around** that bound and both properties hold — the timeout supplies liveness, `Of` +turns whichever termination arrives into an answer. Totality and liveness are different properties; +this section is about the first. ### #3674: the no-op probe is the one that was actually silent, and it is a RE-INSTALL diff --git a/src/MeshWeaver.Mesh.Contract/DetachedReplyOutcome.cs b/src/MeshWeaver.Mesh.Contract/DetachedReplyOutcome.cs index ee839475cd..4867f93fd1 100644 --- a/src/MeshWeaver.Mesh.Contract/DetachedReplyOutcome.cs +++ b/src/MeshWeaver.Mesh.Contract/DetachedReplyOutcome.cs @@ -29,12 +29,19 @@ namespace MeshWeaver.Mesh; /// 🚨 TOTALITY IS NOT LIVENESS, and this type gives only the first. A source that /// never emits AND never completes — Observable.Never, a request whose response is lost, the /// starvation shape behind #2742 -/// — reaches no arm at all, and no composition OVER a source can invent a termination it never -/// produces. Read the guarantee strictly: if the source terminates, exactly one outcome is -/// emitted. A leg whose source can starve still owes its own Timeout, exactly as -/// DispatchInnerCreate (InnerCreateVerdictBound) and the no-op probe -/// (NodeOpForwardTimeout) carry one — and composing through this type neither adds nor -/// excuses that bound. See Doc/Architecture/WriteVerdictTotality → "The FOURTH outcome". +/// — reaches no arm at all, and Of cannot rescue it. Every operator it applies is +/// NOTIFICATION-DRIVEN: each one needs an upstream notification before it can do anything. +/// Take(1) does synthesise a completion — but only after a value; Catch does +/// substitute a sequence — but only after a fault; DefaultIfEmpty fires only on a +/// completion. From a source that has produced NOTHING, none of them can fire at all. Read the +/// guarantee strictly: if the source terminates, exactly one outcome is emitted. +/// Timeout is the exception, and the reason is the useful part — it is CLOCK-driven, so it +/// is the one composition that can terminate a silent source. A leg whose source can starve +/// therefore still owes its own, exactly as DispatchInnerCreate +/// (InnerCreateVerdictBound) and the no-op probe (NodeOpForwardTimeout) carry one. +/// Compose Of AROUND that bound and both properties hold; composing through this type alone +/// neither adds nor excuses it. See Doc/Architecture/WriteVerdictTotality → "The FOURTH +/// outcome". /// /// 🚨 Empty is NOT a value. DefaultIfEmpty() on the source itself would emit /// default(T) — null for a reference type — which the value arm then reads as a real @@ -65,8 +72,10 @@ internal static class DetachedReplyOutcome /// /// value → HasValue, fault → Error, completed empty → CompletedEmpty — one /// emission for each of the three terminations Rx delivers, so a subscriber cannot fail to - /// answer a source that TERMINATES. It adds no deadline: a source that never terminates still - /// reaches nobody, and bounding that is the caller's own job (see the type's remarks). + /// answer a source that TERMINATES. It adds no deadline — every operator it applies is + /// notification-driven, so none of them can fire while the source is silent — and a source that + /// never terminates therefore still reaches nobody. Bounding that needs the one CLOCK-driven + /// operator, a Timeout the caller composes UNDER this (see the type's remarks). /// /// Take(1) comes FIRST so the outcome is the source's own first terminal: a /// source that emits and then faults has already answered, and its late fault must not