git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
commitb97f5c6b15db7eeaf35da93d1a12d61a141df11c authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 00:30 (8 months ago) parentb070f43
4 files changed+192−3
spago.lock+39 spago.yaml+3 src/Yoga/Om/Layer.purs+49−2 test/Test/Main.purs+101−1
spago.lock+39
@@ -8,12 +8,18 @@
88 {
99 "aff": ">=7.1.0 <8.0.0"
1010 },
11+ {
12+ "aff-retry": ">=2.0.0 <3.0.0"
13+ },
1114 {
1215 "arrays": ">=7.3.0 <8.0.0"
1316 },
1417 {
1518 "effect": ">=4.0.0 <5.0.0"
1619 },
20+ {
21+ "exceptions": ">=6.1.0 <7.0.0"
22+ },
1723 {
1824 "foldable-traversable": ">=6.0.0 <7.0.0"
1925 },
@@ -44,6 +50,9 @@
4450 {
4551 "unsafe-coerce": ">=6.0.0 <7.0.0"
4652 },
53+ {
54+ "variant": ">=8.0.0 <9.0.0"
55+ },
4756 {
4857 "yoga-om": ">=1.2.2 <2.0.0"
4958 }
@@ -88,6 +97,26 @@
8897 "unsafe-coerce"
8998 ]
9099 },
100+ "aff-retry": {
101+ "type": "registry",
102+ "version": "2.0.0",
103+ "integrity": "sha256-rKZNcP4IKSyBFQBVzrCvE0ULoJt0N/uDLLJ0dPotr4k=",
104+ "dependencies": [
105+ "aff",
106+ "arrays",
107+ "datetime",
108+ "effect",
109+ "either",
110+ "exceptions",
111+ "integers",
112+ "maybe",
113+ "newtype",
114+ "numbers",
115+ "prelude",
116+ "random",
117+ "transformers"
118+ ]
119+ },
91120 "ansi": {
92121 "type": "registry",
93122 "version": "7.0.0",
@@ -633,6 +662,16 @@
633662 "tuples"
634663 ]
635664 },
665+ "random": {
666+ "type": "registry",
667+ "version": "6.0.0",
668+ "integrity": "sha256-7sZRo6P9UIiXDZ9s2QL9T8h3wlIwLdQrR4+oZKOijQw=",
669+ "dependencies": [
670+ "effect",
671+ "integers",
672+ "prelude"
673+ ]
674+ },
636675 "record": {
637676 "type": "registry",
638677 "version": "4.0.0",
spago.yaml+3
@@ -10,8 +10,10 @@ package:
1010 githubRepo: purescript-yoga-om-layer
1111 dependencies:
1212 - aff: ">=7.1.0 <8.0.0"
13+ - aff-retry: ">=2.0.0 <3.0.0"
1314 - arrays: ">=7.3.0 <8.0.0"
1415 - effect: ">=4.0.0 <5.0.0"
16+ - exceptions: ">=6.1.0 <7.0.0"
1517 - foldable-traversable: ">=6.0.0 <7.0.0"
1618 - integers: ">=6.0.0 <7.0.0"
1719 - maybe: ">=6.0.0 <7.0.0"
@@ -22,6 +24,7 @@ package:
2224 - record-studio: ">=1.0.4 <2.0.0"
2325 - type-equality: ">=4.0.1 <5.0.0"
2426 - unsafe-coerce: ">=6.0.0 <7.0.0"
27+ - variant: ">=8.0.0 <9.0.0"
2528 - yoga-om: ">=1.2.2 <2.0.0"
2629 test:
2730 main: Test.Main
src/Yoga/Om/Layer.purs+49−2
@@ -16,6 +16,8 @@ module Yoga.Om.Layer
1616 , combineRequirements
1717 , provide
1818 , (>->)
19+ , recovering
20+ , repeating
1921 , class CheckAllProvided
2022 , class CheckAllLabelsExist
2123 , class CheckLabelExists
@@ -23,17 +25,19 @@ module Yoga.Om.Layer
2325
2426import Prelude
2527
28+import Control.Monad.Error.Class (catchError, throwError)
2629import Control.Parallel (class Parallel)
2730import Control.Parallel as Parallel
2831import Data.Array as Array
2932import Data.Foldable (for_)
3033import Data.Map (Map)
3134import Data.Map as Map
32−import Data.Maybe (Maybe(..))
35+import Data.Maybe (Maybe(..), maybe)
3336import Data.Nullable (Nullable, toMaybe)
34−import Data.Variant (class VariantMatchCases)
37+import Data.Variant (class VariantMatchCases, onMatch)
3538import Effect (Effect)
3639import Effect.Aff (Aff, bracket)
40+import Effect.Aff.Retry (RetryPolicyM, RetryStatus, applyAndDelay, defaultRetryStatus)
3741import Effect.Class (liftEffect)
3842import Effect.Exception (Error)
3943import Effect.Ref (Ref)
@@ -417,3 +421,46 @@ provide l2 l1 = OmLayer (freshId unit) do
417421 Om.expand (buildLayer l2) # Om.widenCtx prov1
418422
419423infixl 9 provide as >->
424+
425+-- =============================================================================
426+-- Retry / Repeat on layers
427+-- =============================================================================
428+
429+recovering
430+ :: forall req err rest a
431+ (handlersRL :: RowList Type)
432+ (handlers :: Row Type)
433+ (handled :: Row Type)
434+ . RowToList handlers handlersRL
435+ => VariantMatchCases handlersRL handled (Om (Record req) err Boolean)
436+ => Union handled rest (exception :: Error | err)
437+ => RetryPolicyM (Om (Record req) err)
438+ -> (RetryStatus -> Record handlers)
439+ -> OmLayer req err a
440+ -> OmLayer req err a
441+recovering policy mkChecks (OmLayer layerId om) =
442+ OmLayer layerId (go defaultRetryStatus)
443+ where
444+ go status =
445+ catchError om (handleErr status)
446+
447+ handleErr status variant = do
448+ let shouldRetry = variant # onMatch (mkChecks status) (\_ -> pure false)
449+ ifM shouldRetry
450+ (applyAndDelay policy status >>= maybe (throwError variant) go)
451+ (throwError variant)
452+
453+repeating
454+ :: forall req err a
455+ . RetryPolicyM (Om (Record req) err)
456+ -> (RetryStatus -> a -> Om (Record req) err Boolean)
457+ -> OmLayer req err a
458+ -> OmLayer req err a
459+repeating policy shouldRepeat (OmLayer layerId om) =
460+ OmLayer layerId (go defaultRetryStatus)
461+ where
462+ go status = do
463+ result <- om
464+ ifM (shouldRepeat status result)
465+ (applyAndDelay policy status >>= maybe (pure result) go)
466+ (pure result)
test/Test/Main.purs+101−1
@@ -17,7 +17,9 @@ import Test.Spec.Runner (runSpec)
1717import Data.Either (Either(..))
1818import Data.Variant (match)
1919import Yoga.Om as Om
20−import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, fresh, combineRequirements, runLayer, runScoped, runScopedWith, withScoped, provide)
20+import Data.Time.Duration (Milliseconds(..))
21+import Effect.Aff.Retry (RetryStatus(..), limitRetries, constantDelay)
22+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, fresh, combineRequirements, runLayer, runScoped, runScopedWith, withScoped, provide, recovering, repeating)
2123
2224-- Example types
2325type Config = { port :: Int, host :: String }
@@ -516,3 +518,101 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
516518 result.b `shouldEqual` "B"
517519 finalLog <- liftEffect $ Ref.read log
518520 finalLog `shouldEqual` [ "a", "b" ]
521+
522+ describe "Layer recovering" do
523+
524+ it "retries layer construction on matching errors" do
525+ attemptsRef <- liftEffect $ Ref.new 0
526+ let
527+ layer :: OmLayer () (dbError :: String) { db :: String }
528+ layer = makeLayer do
529+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
530+ when (attempts < 3) do
531+ Om.throw { dbError: "connection refused" }
532+ pure { db: "connected" }
533+
534+ retried = layer
535+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 5)
536+ (\_ -> { dbError: \_ -> pure true })
537+
538+ result <- Om.runOm {}
539+ { exception: \_ -> pure { db: "" }, dbError: \_ -> pure { db: "" } }
540+ (runLayer {} retried)
541+ result.db `shouldEqual` "connected"
542+ attempts <- liftEffect $ Ref.read attemptsRef
543+ attempts `shouldEqual` 3
544+
545+ it "does not retry on non-matching errors" do
546+ attemptsRef <- liftEffect $ Ref.new 0
547+ let
548+ layer :: OmLayer () (dbError :: String, authError :: String) { db :: String }
549+ layer = makeLayer do
550+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
551+ Om.throw { authError: "unauthorized" }
552+
553+ retried = layer
554+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 3)
555+ (\_ -> { dbError: \_ -> pure true })
556+
557+ _ <- Om.runOm {}
558+ { exception: \_ -> pure { db: "" }
559+ , dbError: \_ -> pure { db: "" }
560+ , authError: \_ -> pure { db: "" }
561+ }
562+ (runLayer {} retried)
563+ attempts <- liftEffect $ Ref.read attemptsRef
564+ attempts `shouldEqual` 1
565+
566+ it "uses RetryStatus in the check" do
567+ attemptsRef <- liftEffect $ Ref.new 0
568+ let
569+ layer :: OmLayer () (dbError :: String) { db :: String }
570+ layer = makeLayer do
571+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
572+ Om.throw { dbError: "fail" }
573+
574+ retried = layer
575+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 10)
576+ (\(RetryStatus s) -> { dbError: \_ -> pure (s.iterNumber < 2) })
577+
578+ _ <- Om.runOm {}
579+ { exception: \_ -> pure { db: "" }, dbError: \_ -> pure { db: "" } }
580+ (runLayer {} retried)
581+ attempts <- liftEffect $ Ref.read attemptsRef
582+ attempts `shouldEqual` 3
583+
584+ describe "Layer repeating" do
585+
586+ it "repeats layer construction while condition holds" do
587+ counterRef <- liftEffect $ Ref.new 0
588+ let
589+ layer :: OmLayer () () { count :: Int }
590+ layer = makeLayer do
591+ count <- Ref.modify (_ + 1) counterRef # liftEffect
592+ pure { count }
593+
594+ repeated = layer
595+ # repeating (constantDelay (Milliseconds 0.0) <> limitRetries 10)
596+ (\_ r -> pure (r.count < 5))
597+
598+ result <- Om.runOm {}
599+ { exception: \_ -> pure { count: 0 } }
600+ (runLayer {} repeated)
601+ result.count `shouldEqual` 5
602+
603+ it "stops when policy exhausted" do
604+ counterRef <- liftEffect $ Ref.new 0
605+ let
606+ layer :: OmLayer () () { count :: Int }
607+ layer = makeLayer do
608+ count <- Ref.modify (_ + 1) counterRef # liftEffect
609+ pure { count }
610+
611+ repeated = layer
612+ # repeating (constantDelay (Milliseconds 0.0) <> limitRetries 3)
613+ (\_ _ -> pure true)
614+
615+ result <- Om.runOm {}
616+ { exception: \_ -> pure { count: 0 } }
617+ (runLayer {} repeated)
618+ result.count `shouldEqual` 4