Skip to content

Commit 61731ba

Browse files
Add Aff-based API (#35)
* Add bindings to AbortController/AbortSignal * Add signal variant of question' * Add Aff-based APIs * Add changelog entry
1 parent 76aa1fb commit 61731ba

File tree

8 files changed

+214
-1
lines changed

8 files changed

+214
-1
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,23 @@ New features:
3131
- crlfDelay
3232
- escapeCodeTimeout
3333
- tabSize
34-
- Added missing APIs (#35 by @JordanMartinez)
34+
- Added missing APIs (#35, #36 by @JordanMartinez)
3535

3636
- `pause`/`resume`
37+
- `question'`
3738
- `getPrompt`
3839
- `write` exposed as `writeData` and `writeKey`
3940
- `line`, `cursor`
4041
- `getCursorPos`, `clearLine` variants, `clearScreenDown` variants
4142
- `cursorTo` variants, `moveCursor` variants
4243
- `emitKeyPressEvents`
44+
- Added `Aff`-based convenience methods (#36 by @JordanMartinez)
45+
46+
- `question`
47+
- `question'`
48+
- `countLines`
49+
- `blockUntilClosed`
50+
- Added bindings for `AbortController`/`AbortSignal` (#36 by @JordanMartinez)
4351

4452
Bugfixes:
4553

src/Node/Errors/AbortController.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const newImpl = () => new AbortController();
2+
export { newImpl as new };
3+
export const abortImpl = (controller) => controller.abort();
4+
export const abortReasonImpl = (controller, reason) => controller.abort(reason);
5+
export const signal = (controller) => controller.signal;

src/Node/Errors/AbortController.purs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module Node.Errors.AbortController
2+
( AbortController
3+
, new
4+
, abort
5+
, abort'
6+
, signal
7+
) where
8+
9+
import Prelude
10+
11+
import Effect (Effect)
12+
import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
13+
import Node.Errors.AbortSignal (AbortSignal)
14+
15+
foreign import data AbortController :: Type
16+
17+
foreign import new :: Effect (AbortController)
18+
19+
abort :: AbortController -> Effect Unit
20+
abort c = runEffectFn1 abortImpl c
21+
22+
foreign import abortImpl :: EffectFn1 (AbortController) (Unit)
23+
24+
abort' :: forall a. AbortController -> a -> Effect Unit
25+
abort' c reason = runEffectFn2 abortReasonImpl c reason
26+
27+
foreign import abortReasonImpl :: forall a. EffectFn2 (AbortController) (a) (Unit)
28+
29+
foreign import signal :: AbortController -> AbortSignal

src/Node/Errors/AbortSignal.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const newAbort = () => AbortSignal.abort();
2+
export const newAbortReasonImpl = (reason) => AbortSignal.abort(reason);
3+
export const timeoutImpl = (delay) => AbortSignal.timeout(delay);
4+
export const abortedImpl = (sig) => sig.aborted;
5+
export const reasonImpl = (sig) => sig.reason;
6+
export const throwIfAbortedImpl = (sig) => sig.throwIfAborted();

src/Node/Errors/AbortSignal.purs

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Node.Errors.AbortSignal
2+
( AbortSignal
3+
, toEventEmitter
4+
, newAbort
5+
, newAbort'
6+
, newTimeout
7+
, abortH
8+
, aborted
9+
, reason
10+
, throwIfAborted
11+
) where
12+
13+
import Prelude
14+
15+
import Data.Time.Duration (Milliseconds)
16+
import Effect (Effect)
17+
import Effect.Uncurried (EffectFn1, runEffectFn1)
18+
import Foreign (Foreign)
19+
import Node.EventEmitter (EventEmitter, EventHandle(..))
20+
import Node.EventEmitter.UtilTypes (EventHandle0)
21+
import Unsafe.Coerce (unsafeCoerce)
22+
23+
foreign import data AbortSignal :: Type
24+
25+
toEventEmitter :: AbortSignal -> EventEmitter
26+
toEventEmitter = unsafeCoerce
27+
28+
foreign import newAbort :: Effect (AbortSignal)
29+
30+
newAbort' :: forall a. a -> Effect AbortSignal
31+
newAbort' reason' = runEffectFn1 newAbortReasonImpl reason'
32+
33+
foreign import newAbortReasonImpl :: forall a. EffectFn1 (a) (AbortSignal)
34+
35+
newTimeout :: Milliseconds -> Effect AbortSignal
36+
newTimeout delay = runEffectFn1 timeoutImpl delay
37+
38+
foreign import timeoutImpl :: EffectFn1 (Milliseconds) (AbortSignal)
39+
40+
-- | The 'abort' event is emitted when the abortController.abort() method is called. The callback is invoked with a single object argument with a single type property set to 'abort':
41+
-- |
42+
-- | We recommended that code check that the `abortSignal.aborted` attribute is false before adding an 'abort' event listener.
43+
-- |
44+
-- | Any event listeners attached to the AbortSignal should use the { once: true } option (or, if using the EventEmitter APIs to attach a listener, use the once() method) to ensure that the event listener is removed as soon as the 'abort' event is handled. Failure to do so may result in memory leaks.
45+
abortH :: EventHandle0 AbortSignal
46+
abortH = EventHandle "abort" identity
47+
48+
aborted :: AbortSignal -> Effect Boolean
49+
aborted sig = runEffectFn1 abortedImpl sig
50+
51+
foreign import abortedImpl :: EffectFn1 (AbortSignal) (Boolean)
52+
53+
reason :: AbortSignal -> Effect Foreign
54+
reason sig = runEffectFn1 reasonImpl sig
55+
56+
foreign import reasonImpl :: EffectFn1 (AbortSignal) (Foreign)
57+
58+
throwIfAborted :: AbortSignal -> Effect Unit
59+
throwIfAborted sig = runEffectFn1 throwIfAbortedImpl sig
60+
61+
foreign import throwIfAbortedImpl :: EffectFn1 (AbortSignal) (Unit)

src/Node/ReadLine.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const pauseImpl = (rl) => rl.pause();
2525
export const promptImpl = (rl) => rl.prompt();
2626
export const promptOptsImpl = (rl, cursor) => rl.prompt(cursor);
2727
export const questionImpl = (rl, text, cb) => rl.question(text, cb);
28+
export const questionOptsCbImpl = (rl, text, opts, cb) => rl.question(text, opts, cb);
2829
export const resumeImpl = (rl) => rl.resume();
2930
export const setPromptImpl = (rl, prompt) => rl.setPrompt(prompt);
3031
export const getPromptImpl = (rl) => rl.getPrompt();

src/Node/ReadLine.purs

+28
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Node.ReadLine
1818
, crlfDelay
1919
, escapeCodeTimeout
2020
, tabSize
21+
, signal
2122
, closeH
2223
, lineH
2324
, historyH
@@ -31,6 +32,7 @@ module Node.ReadLine
3132
, prompt
3233
, prompt'
3334
, question
35+
, question'
3436
, resume
3537
, setPrompt
3638
, getPrompt
@@ -64,6 +66,7 @@ import Data.Time.Duration (Milliseconds)
6466
import Effect (Effect)
6567
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4)
6668
import Foreign (Foreign)
69+
import Node.Errors.AbortSignal (AbortSignal)
6770
import Node.EventEmitter (EventEmitter, EventHandle(..))
6871
import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1)
6972
import Node.Process (stdin, stdout)
@@ -182,6 +185,9 @@ escapeCodeTimeout = opt "escapeCodeTimeout"
182185
tabSize :: Option InterfaceOptions Int
183186
tabSize = opt "tabSize"
184187

188+
signal :: Option InterfaceOptions AbortSignal
189+
signal = opt "signal"
190+
185191
-- | The 'close' event is emitted when one of the following occur:
186192
-- |
187193
-- | - The `rl.close()` method is called and the readline.Interface instance has relinquished control over the input and output streams;
@@ -314,6 +320,28 @@ question text cb iface = runEffectFn3 questionImpl iface text cb
314320

315321
foreign import questionImpl :: EffectFn3 (Interface) (String) ((String -> Effect Unit)) Unit
316322

323+
-- | Writes a query to the output, waits
324+
-- | for user input to be provided on input, then invokes
325+
-- | the callback function
326+
-- |
327+
-- | Args:
328+
-- | - `query` <string> A statement or query to write to output, prepended to the prompt.
329+
-- | - `options` <Object>
330+
-- | - `signal` <AbortSignal> Optionally allows the question() to be canceled using an AbortController.
331+
-- | - `callback` <Function> A callback function that is invoked with the user's input in response to the query.
332+
-- |
333+
-- | The `rl.question()` method displays the query by writing it to the output, waits for user input to be provided on input, then invokes the callback function passing the provided input as the first argument.
334+
-- |
335+
-- | When called, `rl.question()` will resume the input stream if it has been paused.
336+
-- |
337+
-- | If the readline.Interface was created with output set to null or undefined the query is not written.
338+
-- |
339+
-- | The callback function passed to `rl.question()` does not follow the typical pattern of accepting an Error object or null as the first argument. The callback is called with the provided answer as the only argument.
340+
question' :: String -> { signal :: AbortSignal } -> (String -> Effect Unit) -> Interface -> Effect Unit
341+
question' text opts cb iface = runEffectFn4 questionOptsCbImpl iface text opts cb
342+
343+
foreign import questionOptsCbImpl :: EffectFn4 (Interface) (String) { signal :: AbortSignal } ((String -> Effect Unit)) Unit
344+
317345
-- | The rl.resume() method resumes the input stream if it has been paused.
318346
resume :: Interface -> Effect Unit
319347
resume iface = runEffectFn1 resumeImpl iface

src/Node/ReadLine/Aff.purs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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

Comments
 (0)