@@ -0,0 +1,614 @@
1+module Test.Main where
2+
3+import Prelude
4+
5+import Control.Monad.Error.Class as MonadThrow
6+import Control.Monad.Reader.Class (asks)
7+import Data.Array (intercalate)
8+import Data.Either (Either(..))
9+import Data.Maybe (Maybe(..))
10+import Data.Monoid (power)
11+import Data.Time.Duration (Milliseconds(..), Seconds(..))
12+import Data.Variant (match)
13+import Effect (Effect)
14+import Effect.Aff (Aff, launchAff_)
15+import Effect.Aff as Aff
16+import Effect.Aff.Class (liftAff)
17+import Effect.Aff.Retry (RetryStatus(..), limitRetries, constantDelay)
18+import Effect.Class (liftEffect)
19+import Effect.Class.Console as Console
20+import Effect.Exception as Exn
21+import Effect.Ref as Ref
22+import Test.Spec (describe, it)
23+import Test.Spec.Assertions (shouldEqual)
24+import Test.Spec.Reporter (consoleReporter)
25+import Test.Spec.Runner (runSpec)
26+import Yoga.Om (Om)
27+import Yoga.Om as Om
28+import Yoga.Om.Retry (recovering, repeating)
29+
30+main :: Effect Unit
31+main = launchAff_ $ runSpec [ consoleReporter ] do
32+
33+ describe "Yoga.Om - basics" do
34+
35+ it "pure values pass through" do
36+ result <- pure true
37+ # Om.runOm {} { exception: \_ -> pure false }
38+ # liftAff
39+ result `shouldEqual` true
40+
41+ it "launches safely" do
42+ let launch = Console.log "Hello from Om" # Om.launchOm_ {} { exception: \_ -> pure unit }
43+ liftEffect launch
44+
45+ it "reads context with ask" do
46+ result <- do
47+ ctx <- Om.ask
48+ pure ctx.name
49+ # Om.runOm { name: "Alice" } { exception: \_ -> pure "" }
50+ # liftAff
51+ result `shouldEqual` "Alice"
52+
53+ it "lifts Aff with fromAff" do
54+ ref <- Ref.new 0 # liftEffect
55+ result <- Om.fromAff (Ref.modify (_ + 1) ref # liftEffect)
56+ # Om.runOm {} { exception: \_ -> pure 0 }
57+ # liftAff
58+ result `shouldEqual` 1
59+
60+ it "runOmOrThrow runs Aff-backed Om safely" do
61+ result <- Om.runOmOrThrow {} do
62+ Om.delay (Milliseconds 0.0)
63+ pure 42
64+ result `shouldEqual` 42
65+
66+ it "runOmOrThrow still runs lifted Effect code" do
67+ ref <- Ref.new 0 # liftEffect
68+ result <- Om.runOmOrThrow {} do
69+ liftEffect $ Ref.modify_ (_ + 1) ref
70+ pure 7
71+ result `shouldEqual` 7
72+ final <- Ref.read ref # liftEffect
73+ final `shouldEqual` 1
74+
75+ describe "Yoga.Om - exception safety" do
76+
77+ it "fatal throws a string into the exception channel" do
78+ result <- do
79+ _ <- Om.fatal "something went wrong"
80+ pure "unreachable"
81+ # Om.runOm {} { exception: \e -> pure (Exn.message e) }
82+ # liftAff
83+ result `shouldEqual` "something went wrong"
84+
85+ it "catches Aff exceptions" do
86+ result <- do
87+ _ <- Om.fromAff (Aff.throwError (Exn.error "boom"))
88+ pure "ok"
89+ # Om.runOm {} { exception: \_ -> pure "caught" }
90+ # liftAff
91+ result `shouldEqual` "caught"
92+
93+ it "catches Effect exceptions" do
94+ result <- do
95+ liftEffect throwingEffect $> "never reached"
96+ # Om.runOm {} { exception: \_ -> pure "caught" }
97+ # liftAff
98+ result `shouldEqual` "caught"
99+
100+ describe "Yoga.Om - context composition" do
101+
102+ it "combines Oms with different contexts" do
103+ let askText = Om.ask <#> _.text
104+ let askHowOften = Om.asks _.howOften
105+ result <- do
106+ text <- askText
107+ times <- askHowOften
108+ pure (power text times)
109+ # Om.runOm { text: "!", howOften: 3 } { exception: \_ -> pure "" }
110+ # liftAff
111+ result `shouldEqual` "!!!"
112+
113+ it "expandCtx widens a closed context" do
114+ let appM1 = (asks _.x) :: Om { x :: String } () String
115+ let appM2 = (asks _.y) :: Om { y :: String } () String
116+ result <- do
117+ r1 <- Om.expandCtx appM1
118+ r2 <- Om.expandCtx appM2
119+ pure (r1 <> " " <> r2)
120+ # Om.runOm { x: "good", y: "food" } { exception: \_ -> pure "" }
121+ # liftAff
122+ result `shouldEqual` "good food"
123+
124+ it "expand widens both context and errors" do
125+ let appM1 = (asks _.x) :: Om { x :: String } (bad :: String) String
126+ let appM2 = (asks _.y) :: Om { y :: String } (realBad :: String) String
127+ result <- do
128+ r1 <- Om.expand appM1
129+ r2 <- Om.expand appM2
130+ pure (r1 <> " " <> r2)
131+ # Om.runOm { x: "good", y: "food" } { exception: \_ -> pure "", bad: pure, realBad: pure }
132+ # liftAff
133+ result `shouldEqual` "good food"
134+
135+ describe "Yoga.Om - errors" do
136+
137+ it "throw delivers error to handler" do
138+ result <- Om.throw { notFound: "missing" }
139+ # Om.runOm {} { exception: \_ -> pure "", notFound: \msg -> pure msg }
140+ # liftAff
141+ result `shouldEqual` "missing"
142+
143+ it "throw with error helper" do
144+ result <- (MonadThrow.throwError $ Om.error { badError: "bad" })
145+ # runExampleOm
146+ # liftAff
147+ result `shouldEqual` "bad"
148+
149+ it "expandErr widens closed error rows" do
150+ let appM1 = pure "good" :: Om {} (goodError :: Int) String
151+ let appM2 = Om.throw { badError: "bad" } :: Om {} (badError :: String) String
152+ result <- do
153+ _ <- Om.expandErr appM1
154+ Om.expandErr appM2
155+ # Om.runOm {} { exception: \_ -> pure "", goodError: pure <<< show, badError: pure }
156+ # liftAff
157+ result `shouldEqual` "bad"
158+
159+ it "handleErrors catches and recovers" do
160+ result <- Om.handleErrors { notFound: \_ -> pure "recovered" } do
161+ Om.throw { notFound: "oops" }
162+ # Om.runOm {} { exception: \_ -> pure "" }
163+ # liftAff
164+ result `shouldEqual` "recovered"
165+
166+ it "handleErrors eliminates handled error from row" do
167+ let
168+ getFromCache = do
169+ _ <- Om.throw { cacheError: "not found" }
170+ _ <- Om.throw { benignError: "harmless" }
171+ Om.throw { badError: "bad" }
172+ let
173+ appM :: Om {} (badError :: String) String
174+ appM = do
175+ _ <- getFromCache # Om.handleErrors
176+ { cacheError: \_ -> pure "cached"
177+ , benignError: \_ -> pure "ok"
178+ }
179+ pure "done"
180+ result <- appM # runExampleOm # liftAff
181+ result `shouldEqual` "done"
182+
183+ it "handleErrors can transform errors" do
184+ let
185+ getFromCache = do
186+ _ <- Om.throw { cacheError: "not found" }
187+ Om.throw { fatal: "horrible" }
188+ let
189+ appM :: Om {} (badError :: String) String
190+ appM = do
191+ _ <- getFromCache # Om.handleErrors
192+ { cacheError: \_ -> Om.throw { badError: "bad" }
193+ , fatal: \_ -> pure "fine"
194+ }
195+ pure "done"
196+ result <- appM # runExampleOm # liftAff
197+ result `shouldEqual` "bad"
198+
199+ it "mapError transforms error type" do
200+ result <- Om.mapError @"notFound" @"apiError" (\msg -> "mapped: " <> msg)
201+ (Om.throw { notFound: "gone" })
202+ # Om.runOm {} { exception: \_ -> pure "", apiError: \msg -> pure msg }
203+ # liftAff
204+ result `shouldEqual` "mapped: gone"
205+
206+ it "onError runs side effect then re-throws" do
207+ ref <- Ref.new "" # liftEffect
208+ result <- Om.onError @"notFound"
209+ (\msg -> do
210+ Ref.write ("saw: " <> msg) ref # liftEffect
211+ Om.throw { notFound: msg }
212+ )
213+ (Om.throw { notFound: "404" })
214+ # Om.runOm {} { exception: \_ -> pure "", notFound: \msg -> pure msg }
215+ # liftAff
216+ result `shouldEqual` "404"
217+ logged <- Ref.read ref # liftEffect
218+ logged `shouldEqual` "saw: 404"
219+
220+ it "combines Oms with different errors" do
221+ let readFromDisk = Om.throw { ioError: { reason: "Disk Exploded", code: -3 } }
222+ let readFromDB = Om.throw { dbError: "DB Dead" }
223+ result <- do
224+ _ <- readFromDisk
225+ _ <- readFromDB
226+ pure "unreachable"
227+ # Om.runOm {}
228+ { exception: \_ -> pure ""
229+ , dbError: pure
230+ , ioError: \err -> pure (err.reason <> " " <> show err.code)
231+ }
232+ # liftAff
233+ result `shouldEqual` "Disk Exploded -3"
234+
235+ describe "Yoga.Om - helpers" do
236+
237+ it "note converts Just to pure" do
238+ result <- Om.note { missing: unit } (Just 42)
239+ # Om.runOm {} { exception: \_ -> pure 0, missing: \(_ :: Unit) -> pure 0 }
240+ # liftAff
241+ result `shouldEqual` 42
242+
243+ it "note converts Nothing to throw" do
244+ result <- Om.note { missing: unit } (Nothing :: Maybe Int)
245+ # Om.runOm {} { exception: \_ -> pure 0, missing: \(_ :: Unit) -> pure 99 }
246+ # liftAff
247+ result `shouldEqual` 99
248+
249+ it "throwWhen throws only when its condition is true" do
250+ let
251+ run condition = do
252+ Om.throwWhen { rejected: "blocked" } condition
253+ pure "continued"
254+ handle =
255+ Om.runOm {}
256+ { exception: \_ -> pure "exception"
257+ , rejected: pure
258+ }
259+ whenTrue <- run true # handle # liftAff
260+ whenFalse <- run false # handle # liftAff
261+ whenTrue `shouldEqual` "blocked"
262+ whenFalse `shouldEqual` "continued"
263+
264+ it "throwUnless throws only when its condition is false" do
265+ let
266+ run condition = do
267+ Om.throwUnless { rejected: "blocked" } condition
268+ pure "continued"
269+ handle =
270+ Om.runOm {}
271+ { exception: \_ -> pure "exception"
272+ , rejected: pure
273+ }
274+ whenTrue <- run true # handle # liftAff
275+ whenFalse <- run false # handle # liftAff
276+ whenTrue `shouldEqual` "continued"
277+ whenFalse `shouldEqual` "blocked"
278+
279+ it "throwLeftAs converts Left to error" do
280+ result <- Left "bad" # Om.throwLeftAs (\badError -> Om.error { badError })
281+ # runExampleOm
282+ # liftAff
283+ result `shouldEqual` "bad"
284+
285+ it "noteM throws when Nothing with monadic error" do
286+ result <- (Nothing # Om.noteM do
287+ _ <- Om.throw { badError: "bad" }
288+ pure $ Om.error { badError: "unreachable" })
289+ # runExampleOm
290+ # liftAff
291+ result `shouldEqual` "bad"
292+
293+ it "throwLeftAsM converts Left with monadic transform" do
294+ result <- (Left "bad" # Om.throwLeftAsM \badError -> do
295+ _ <- Om.throw { badError }
296+ pure $ Om.error { badError: "unreachable" })
297+ # runExampleOm
298+ # liftAff
299+ result `shouldEqual` "bad"
300+
301+ it "tap runs side effect and returns value" do
302+ ref <- Ref.new "" # liftEffect
303+ result <- Om.tap (\s -> Ref.write s ref # liftEffect) "hello"
304+ # Om.runOm {} { exception: \_ -> pure "" }
305+ # liftAff
306+ result `shouldEqual` "hello"
307+ written <- Ref.read ref # liftEffect
308+ written `shouldEqual` "hello"
309+
310+ it "tapM runs side effect on monadic result" do
311+ ref <- Ref.new 0 # liftEffect
312+ result <- Om.tapM (\n -> Ref.write n ref # liftEffect) (pure 42)
313+ # Om.runOm {} { exception: \_ -> pure 0 }
314+ # liftAff
315+ result `shouldEqual` 42
316+ written <- Ref.read ref # liftEffect
317+ written `shouldEqual` 42
318+
319+ describe "Yoga.Om - parallel" do
320+
321+ it "race returns fastest result" do
322+ result <- Om.race
323+ [ Om.delay (1.0 # Seconds) *> pure "slow"
324+ , pure "fast"
325+ ]
326+ # runExampleOm
327+ # liftAff
328+ result `shouldEqual` "fast"
329+
330+ it "race prefers success over fast failure" do
331+ result <- Om.race
332+ [ Om.throw { badError: "fast but broken" }
333+ , Om.delay (8.0 # Milliseconds) *> pure "slow but works"
334+ ]
335+ # runExampleOm
336+ # liftAff
337+ result `shouldEqual` "slow but works"
338+
339+ it "inParallel collects all results" do
340+ result <- do
341+ strings <- Om.inParallel
342+ [ Om.delay (9.0 # Milliseconds) *> pure "1"
343+ , Om.delay (1.0 # Milliseconds) *> pure "2"
344+ ]
345+ pure $ intercalate "|" strings
346+ # runExampleOm
347+ # liftAff
348+ result `shouldEqual` "1|2"
349+
350+ it "inParallel fails fast" do
351+ result <- do
352+ strings <- Om.inParallel
353+ [ Om.throw { badError: "fast but broken" }
354+ , Om.delay (8.0 # Milliseconds) *> pure "slow but works"
355+ ]
356+ pure $ intercalate "|" strings
357+ # runExampleOm
358+ # liftAff
359+ result `shouldEqual` "fast but broken"
360+
361+ describe "Yoga.Om - runOmRecord" do
362+
363+ it "sequences a record of Oms and runs to Aff" do
364+ result <- Om.runOmRecord { greeting: "hi" }
365+ { exception: \_ -> pure { a: "", b: 0 } }
366+ { a: (Om.ask :: Om.Om { greeting :: String } () { greeting :: String }) <#> _.greeting
367+ , b: pure 42 :: Om.Om {} () Int
368+ }
369+ # liftAff
370+ result.a `shouldEqual` "hi"
371+ result.b `shouldEqual` 42
372+
373+ describe "Yoga.Om - sequenceOmRecord" do
374+
375+ it "sequences a record of Oms into one Om" do
376+ result <- Om.sequenceOmRecord
377+ { a: (Om.ask :: Om.Om { x :: Int } () { x :: Int }) <#> _.x
378+ , b: (Om.ask :: Om.Om { y :: String } () { y :: String }) <#> _.y
379+ }
380+ # Om.runOm { x: 1, y: "two" } { exception: \_ -> pure { a: 0, b: "" } }
381+ # liftAff
382+ result.a `shouldEqual` 1
383+ result.b `shouldEqual` "two"
384+
385+ describe "Yoga.Om - runOms" do
386+
387+ it "maps a record of Oms to a record of Affs" do
388+ let
389+ affs = Om.runOms { greeting: "hello" }
390+ { exception: \_ -> pure unit }
391+ { name: (Om.ask :: Om.Om { greeting :: String } () { greeting :: String }) <#> _.greeting
392+ , number: pure 42 :: Om.Om {} () Int
393+ }
394+ name <- affs.name # liftAff
395+ number <- affs.number # liftAff
396+ name `shouldEqual` "hello"
397+ number `shouldEqual` 42
398+
399+ it "runs each Aff independently" do
400+ ref <- Ref.new 0 # liftEffect
401+ let
402+ affs = Om.runOms {}
403+ { exception: \_ -> pure unit }
404+ { inc: Ref.modify (_ + 1) ref # liftEffect :: Om.Om {} () Int
405+ }
406+ _ <- affs.inc # liftAff
407+ _ <- affs.inc # liftAff
408+ _ <- affs.inc # liftAff
409+ result <- Ref.read ref # liftEffect
410+ result `shouldEqual` 3
411+
412+ it "expands different context rows to a shared context" do
413+ let
414+ affs = Om.runOms { foo: 1, bar: "hi" }
415+ { exception: \_ -> pure unit }
416+ { fromFoo: (Om.ask :: Om.Om { foo :: Int } () { foo :: Int }) <#> _.foo
417+ , fromBar: (Om.ask :: Om.Om { bar :: String } () { bar :: String }) <#> _.bar
418+ }
419+ foo <- affs.fromFoo # liftAff
420+ bar <- affs.fromBar # liftAff
421+ foo `shouldEqual` 1
422+ bar `shouldEqual` "hi"
423+
424+ describe "Yoga.Om - runOmsInOm" do
425+
426+ it "runs Oms using context from ask" do
427+ affs <- Om.runOmsInOm
428+ { exception: \_ -> pure unit }
429+ { name: (Om.ask :: Om.Om { greeting :: String } () { greeting :: String }) <#> _.greeting
430+ , number: pure 42 :: Om.Om {} () Int
431+ }
432+ # Om.runOm { greeting: "world" }
433+ { exception: \_ -> pure { name: pure "", number: pure 0 } }
434+ # liftAff
435+ name <- affs.name # liftAff
436+ number <- affs.number # liftAff
437+ name `shouldEqual` "world"
438+ number `shouldEqual` 42
439+
440+ describe "Yoga.Om.Retry - recovering" do
441+
442+ it "retries on matching errors up to the limit" do
443+ attemptsRef <- Ref.new 0 # liftEffect
444+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 3)
445+ (\_ -> { networkError: \(_ :: String) -> pure true })
446+ (\_ -> do
447+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
448+ Om.throw { networkError: "fail " <> show attempts }
449+ )
450+ # Om.runOm {}
451+ { exception: \_ -> pure unit
452+ , networkError: \_ -> pure unit
453+ }
454+ # liftAff
455+ attempts <- Ref.read attemptsRef # liftEffect
456+ attempts `shouldEqual` 4
457+
458+ it "does not retry on non-matching errors" do
459+ attemptsRef <- Ref.new 0 # liftEffect
460+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 3)
461+ (\_ -> { networkError: \(_ :: String) -> pure true })
462+ (\_ -> do
463+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
464+ Om.throw { authError: "unauthorized" }
465+ )
466+ # Om.runOm {}
467+ { exception: \_ -> pure unit
468+ , networkError: \_ -> pure unit
469+ , authError: \_ -> pure unit
470+ }
471+ # liftAff
472+ attempts <- Ref.read attemptsRef # liftEffect
473+ attempts `shouldEqual` 1
474+
475+ it "succeeds after transient failures" do
476+ attemptsRef <- Ref.new 0 # liftEffect
477+ result <-
478+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 5)
479+ (\_ -> { networkError: \(_ :: String) -> pure true })
480+ (\_ -> do
481+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
482+ when (attempts < 3) do
483+ Om.throw { networkError: "transient" }
484+ pure "success"
485+ )
486+ # Om.runOm {}
487+ { exception: \_ -> pure "failed"
488+ , networkError: \_ -> pure "failed"
489+ }
490+ # liftAff
491+ result `shouldEqual` "success"
492+ attempts <- Ref.read attemptsRef # liftEffect
493+ attempts `shouldEqual` 3
494+
495+ it "preserves the error when retries are exhausted" do
496+ result <-
497+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 2)
498+ (\_ -> { networkError: \(_ :: String) -> pure true })
499+ (\_ -> Om.throw { networkError: "server down" })
500+ # Om.runReader {}
501+ # liftAff
502+ case result of
503+ Left err -> do
504+ let msg = err # match
505+ { exception: \_ -> "exception"
506+ , networkError: \e -> e
507+ }
508+ msg `shouldEqual` "server down"
509+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
510+
511+ it "only retries the specified error in a multi-error layer" do
512+ attemptsRef <- Ref.new 0 # liftEffect
513+ result <-
514+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 5)
515+ (\_ -> { networkError: \(_ :: String) -> pure true })
516+ (\_ -> do
517+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
518+ if attempts <= 2
519+ then Om.throw { networkError: "timeout" }
520+ else Om.throw { authError: "forbidden" }
521+ )
522+ # Om.runReader {}
523+ # liftAff
524+ case result of
525+ Left err -> do
526+ let msg = err # match
527+ { exception: \_ -> "exception"
528+ , networkError: \e -> e
529+ , authError: \e -> e
530+ }
531+ msg `shouldEqual` "forbidden"
532+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
533+ attempts <- Ref.read attemptsRef # liftEffect
534+ attempts `shouldEqual` 3
535+
536+ it "uses RetryStatus to decide whether to retry" do
537+ statusesRef <- Ref.new [] # liftEffect
538+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 3)
539+ (\(RetryStatus s) ->
540+ { networkError: \(_ :: Unit) -> do
541+ Ref.modify_ (_ <> [s.iterNumber]) statusesRef # liftEffect
542+ pure true
543+ })
544+ (\_ -> Om.throw { networkError: unit })
545+ # Om.runOm {}
546+ { exception: \_ -> pure unit
547+ , networkError: \_ -> pure unit
548+ }
549+ # liftAff
550+ statuses <- Ref.read statusesRef # liftEffect
551+ statuses `shouldEqual` [0, 1, 2, 3]
552+
553+ it "stops retrying when check returns false" do
554+ attemptsRef <- Ref.new 0 # liftEffect
555+ recovering (constantDelay (Milliseconds 0.0) <> limitRetries 10)
556+ (\(RetryStatus s) ->
557+ { networkError: \(_ :: Unit) -> pure (s.iterNumber < 2) })
558+ (\_ -> do
559+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
560+ Om.throw { networkError: unit }
561+ )
562+ # Om.runOm {}
563+ { exception: \_ -> pure unit
564+ , networkError: \_ -> pure unit
565+ }
566+ # liftAff
567+ attempts <- Ref.read attemptsRef # liftEffect
568+ attempts `shouldEqual` 3
569+
570+ describe "Yoga.Om.Retry - repeating" do
571+
572+ it "repeats while condition holds" do
573+ counterRef <- Ref.new 0 # liftEffect
574+ result <-
575+ repeating (constantDelay (Milliseconds 0.0) <> limitRetries 10)
576+ (\_ n -> pure (n < 5))
577+ (\_ -> Ref.modify (_ + 1) counterRef # liftEffect)
578+ # Om.runOm {} { exception: \_ -> pure 0 }
579+ # liftAff
580+ result `shouldEqual` 5
581+
582+ it "stops when condition returns false" do
583+ counterRef <- Ref.new 0 # liftEffect
584+ result <-
585+ repeating (constantDelay (Milliseconds 0.0) <> limitRetries 100)
586+ (\_ n -> pure (n < 3))
587+ (\_ -> Ref.modify (_ + 1) counterRef # liftEffect)
588+ # Om.runOm {} { exception: \_ -> pure 0 }
589+ # liftAff
590+ result `shouldEqual` 3
591+
592+ it "stops when policy exhausted" do
593+ counterRef <- Ref.new 0 # liftEffect
594+ result <-
595+ repeating (constantDelay (Milliseconds 0.0) <> limitRetries 3)
596+ (\_ _ -> pure true)
597+ (\_ -> Ref.modify (_ + 1) counterRef # liftEffect)
598+ # Om.runOm {} { exception: \_ -> pure 0 }
599+ # liftAff
600+ result `shouldEqual` 4
601+
602+ it "returns last result" do
603+ result <-
604+ repeating (constantDelay (Milliseconds 0.0) <> limitRetries 2)
605+ (\_ _ -> pure true)
606+ (\(RetryStatus s) -> pure ("attempt-" <> show s.iterNumber))
607+ # Om.runOm {} { exception: \_ -> pure "" }
608+ # liftAff
609+ result `shouldEqual` "attempt-2"
610+
611+foreign import throwingEffect :: Effect Unit
612+
613+runExampleOm :: Om {} (badError :: String) String -> Aff String
614+runExampleOm = Om.runOm {} { exception: \_ -> pure "exception", badError: pure }