|
| 1 | +module Node.ReadLine.Aff |
| 2 | + ( question |
| 3 | + , question' |
| 4 | + , blockUntilClosed |
| 5 | + , countLines |
| 6 | + ) where |
| 7 | + |
| 8 | +import Prelude |
| 9 | + |
| 10 | +import Data.Either (Either(..)) |
| 11 | +import Effect.Aff (Aff, effectCanceler, error, makeAff, nonCanceler) |
| 12 | +import Effect.Class (liftEffect) |
| 13 | +import Effect.Exception (Error, throw) |
| 14 | +import Effect.Ref as Ref |
| 15 | +import Effect.Uncurried (mkEffectFn1) |
| 16 | +import Node.Errors.AbortController (AbortController, abort', signal) |
| 17 | +import Node.Errors.AbortSignal (abortH, aborted) |
| 18 | +import Node.EventEmitter (EventHandle(..), on, once) |
| 19 | +import Node.EventEmitter.UtilTypes (EventHandle1) |
| 20 | +import Node.ReadLine (Interface, closeH, lineH) |
| 21 | +import Node.ReadLine as RL |
| 22 | + |
| 23 | +-- | Blocks until receives user input. There is no way to cancel this. |
| 24 | +question :: String -> Interface -> Aff String |
| 25 | +question txt iface = makeAff \done -> do |
| 26 | + RL.question txt (done <<< Right) iface |
| 27 | + pure nonCanceler |
| 28 | + |
| 29 | +-- | Blocks until receives user input. An `AbortController` can be used to cancel this. |
| 30 | +-- | If the `AbortSignal` is aborted outside of this function, this computation |
| 31 | +-- | will produce an error. If the `AbortSignal` is already aborted, this will throw an error. |
| 32 | +question' :: String -> AbortController -> Interface -> Aff String |
| 33 | +question' txt controller iface = do |
| 34 | + let sig = signal controller |
| 35 | + -- Node docs: |
| 36 | + -- > We recommended that code check that the `abortSignal.aborted` |
| 37 | + -- > attribute is `false` before adding an 'abort' event listener. |
| 38 | + liftEffect do |
| 39 | + alreadyAborted <- aborted sig |
| 40 | + when alreadyAborted do |
| 41 | + throw "Signal was already aborted before calling 'question'" |
| 42 | + makeAff \done -> do |
| 43 | + rmAbortListener <- sig # once abortH do |
| 44 | + done $ Left $ error "Signal was aborted after calling 'question'" |
| 45 | + RL.question' txt { signal: sig } (done <<< Right) iface |
| 46 | + pure $ effectCanceler do |
| 47 | + rmAbortListener |
| 48 | + abort' controller "Cancelled" |
| 49 | + |
| 50 | +blockUntilClosed :: Interface -> Aff Unit |
| 51 | +blockUntilClosed iface = makeAff \done -> do |
| 52 | + rmListener <- iface # once closeH (done $ Right unit) |
| 53 | + pure $ effectCanceler rmListener |
| 54 | + |
| 55 | +-- Note: I'm not sure if this is needed, but it's not clear |
| 56 | +-- from the Node docs that a `close` event will occur |
| 57 | +-- if there's an error in either the `input` or `output` streams. |
| 58 | +-- Moreover, `EventEmitter` docs say it's best practices to listen |
| 59 | +-- for `error` events. |
| 60 | +-- > As a best practice, listeners should always be added for the 'error' events. |
| 61 | +errorH :: EventHandle1 Interface Error |
| 62 | +errorH = EventHandle "error" mkEffectFn1 |
| 63 | + |
| 64 | +countLines :: Interface -> Aff Int |
| 65 | +countLines iface = makeAff \done -> do |
| 66 | + countRef <- Ref.new 0 |
| 67 | + rmErrListener <- iface # once errorH (done <<< Left) |
| 68 | + rmCloseListener <- iface # once closeH do |
| 69 | + rmErrListener |
| 70 | + done <<< Right =<< Ref.read countRef |
| 71 | + rmLineListener <- iface # on lineH \_ -> Ref.modify_ (_ + 1) countRef |
| 72 | + pure $ effectCanceler do |
| 73 | + rmErrListener |
| 74 | + rmCloseListener |
| 75 | + rmLineListener |
0 commit comments