git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
commit3e2f690bb24587ad32ea318772f4b52b8b3bdddb authorMark <mark.eibes@gmail.com> dateFri 13 Feb 2026, 20:30 (8 months ago) parent7222d5b
5 files changed+287−125
.psc-ide-port+1 spago.lock+9 spago.yaml+3 src/Yoga/Om/Layer.purs+148−103 test/Test/Main.purs+126−22
.psc-ide-portnew file+1
@@ -0,0 +1 @@
1+15820
spago.lock+9
@@ -8,6 +8,12 @@
88 {
99 "aff": ">=7.1.0 <8.0.0"
1010 },
11+ {
12+ "arrays": ">=7.3.0 <8.0.0"
13+ },
14+ {
15+ "foldable-traversable": ">=6.0.0 <7.0.0"
16+ },
1117 {
1218 "newtype": ">=5.0.0 <6.0.0"
1319 },
@@ -20,6 +26,9 @@
2026 {
2127 "record-studio": ">=1.0.4 <2.0.0"
2228 },
29+ {
30+ "tuples": ">=7.0.0 <8.0.0"
31+ },
2332 {
2433 "type-equality": ">=4.0.1 <5.0.0"
2534 },
spago.yaml+3
@@ -10,10 +10,13 @@ package:
1010 githubRepo: purescript-yoga-om-layer
1111 dependencies:
1212 - aff: ">=7.1.0 <8.0.0"
13+ - arrays: ">=7.3.0 <8.0.0"
14+ - foldable-traversable: ">=6.0.0 <7.0.0"
1315 - newtype: ">=5.0.0 <6.0.0"
1416 - prelude: ">=6.0.2 <7.0.0"
1517 - record: ">=4.0.0 <5.0.0"
1618 - record-studio: ">=1.0.4 <2.0.0"
19+ - tuples: ">=7.0.0 <8.0.0"
1720 - type-equality: ">=4.0.1 <5.0.0"
1821 - unsafe-coerce: ">=6.0.0 <7.0.0"
1922 - yoga-om: ">=1.2.2 <2.0.0"
src/Yoga/Om/Layer.purs+148−103
@@ -1,9 +1,13 @@
11-- @inline export makeLayer arity=1
22-- @inline export runLayer arity=2
3−module Yoga.Om.Layer
3+module Yoga.Om.Layer
44 ( OmLayer(..)
5+ , Finalizers(..)
56 , makeLayer
7+ , makeScopedLayer
8+ , bracketLayer
69 , runLayer
10+ , runScoped
711 , combineRequirements
812 , provide
913 , (>->)
@@ -14,7 +18,11 @@ module Yoga.Om.Layer
1418
1519import Prelude
1620
21+import Data.Array as Array
1722import Data.Newtype (class Newtype)
23+import Data.Tuple.Nested ((/\), type (/\))
24+import Data.Foldable (for_)
25+import Effect.Aff (Aff, generalBracket)
1826import Effect.Aff.Class (liftAff)
1927import Prim.Row (class Nub, class Union)
2028import Prim.RowList (class RowToList, Cons, Nil, RowList)
@@ -26,21 +34,71 @@ import Unsafe.Coerce (unsafeCoerce)
2634import Yoga.Om (Om)
2735import Yoga.Om as Om
2836
29−-- | A layer that requires dependencies (req) and provides services (prov)
30−newtype OmLayer req prov err = OmLayer (Om (Record req) err (Record prov))
37+-- =============================================================================
38+-- Finalizers — guaranteed cleanup actions collected during layer construction
39+-- =============================================================================
40+
41+-- | Cleanup actions to run when a scope closes. Stored in construction order;
42+-- | executed in reverse (LIFO) by `runScoped`.
43+newtype Finalizers = Finalizers (Array (Aff Unit))
44+
45+derive instance Newtype Finalizers _
46+
47+instance Semigroup Finalizers where
48+ append (Finalizers a) (Finalizers b) = Finalizers (a <> b)
49+
50+instance Monoid Finalizers where
51+ mempty = Finalizers []
52+
53+-- =============================================================================
54+-- OmLayer — a layer that requires dependencies, provides services, and
55+-- carries finalizers for resource cleanup
56+-- =============================================================================
57+
58+-- | A layer that requires dependencies (req), provides services (prov),
59+-- | and may carry finalizers for scoped resource management.
60+newtype OmLayer req prov err = OmLayer (Om (Record req) err (Record prov /\ Finalizers))
3161
3262derive instance Newtype (OmLayer req prov err) _
3363
34−-- | Helper to create a layer from an Om computation
64+-- =============================================================================
65+-- Layer constructors
66+-- =============================================================================
67+
68+-- | Create a layer from an Om computation (no finalizers).
3569makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
36−makeLayer = OmLayer
70+makeLayer om = OmLayer (om <#> \r -> r /\ mempty)
71+
72+-- | Create a scoped layer with acquire/release semantics.
73+-- | The release function is guaranteed to run when `runScoped` closes the scope.
74+makeScopedLayer
75+ :: forall req prov err
76+ . Om (Record req) err (Record prov)
77+ -> (Record prov -> Aff Unit)
78+ -> OmLayer req prov err
79+makeScopedLayer acquire release = OmLayer do
80+ prov <- acquire
81+ pure (prov /\ Finalizers [ release prov ])
82+
83+-- | Bracket-style scoped layer: acquire a resource, build provisions from it,
84+-- | and register a finalizer for the resource.
85+bracketLayer
86+ :: forall req prov resource err
87+ . Om (Record req) err resource
88+ -> (resource -> Aff Unit)
89+ -> (resource -> Om (Record req) err (Record prov))
90+ -> OmLayer req prov err
91+bracketLayer acquire release use = OmLayer do
92+ resource <- acquire
93+ prov <- use resource
94+ pure (prov /\ Finalizers [ release resource ])
3795
3896-- =============================================================================
3997-- Custom Type Errors for Missing Dependencies
4098-- =============================================================================
4199
42−-- | Check if all required dependencies are provided
43−-- If not, show a helpful diff of required vs available
100+-- | Check if all required dependencies are provided.
101+-- If not, show a helpful diff of required vs available.
44102class CheckAllProvided (required :: Row Type) (available :: Row Type)
45103
46104instance
@@ -50,157 +108,144 @@ instance
50108 ) =>
51109 CheckAllProvided required available
52110
53−-- | Check that all labels in required RowList exist in available RowList
54−-- Pass along the original rows for better error messages
111+-- | Check that all labels in required RowList exist in available RowList.
112+-- Pass along the original rows for better error messages.
55113class CheckAllLabelsExist (required :: RowList Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
56114
57−-- Base case: no more requirements to check
58115instance CheckAllLabelsExist Nil available requiredRow availableRow
59116
60−-- Recursive case: check if this label exists, then check the rest
61117instance
62118 ( CheckLabelExists label ty available requiredRow availableRow
63119 , CheckAllLabelsExist tail available requiredRow availableRow
64120 ) =>
65121 CheckAllLabelsExist (Cons label ty tail) available requiredRow availableRow
66122
67−-- | Check if a single label exists in the available RowList
68−-- This is where the magic happens - we dispatch on RowList structure in the instance head!
123+-- | Check if a single label exists in the available RowList.
69124class CheckLabelExists (label :: Symbol) (ty :: Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
70125
71−-- Found it! The label matches in the instance head
72−-- TypeEquals ensures the types at this label actually match!
73126instance
74127 TypeEquals ty ty' =>
75128 CheckLabelExists label ty (Cons label ty' tail) requiredRow availableRow
76129
77−-- Keep looking: different label, recurse on tail
78−else instance checkLabelExistsKeepLooking ::
130+else instance
79131 CheckLabelExists label ty tail requiredRow availableRow =>
80132 CheckLabelExists label ty (Cons otherLabel otherTy tail) requiredRow availableRow
81133
82−-- Not found: reached end of list, emit custom error WITH FULL CONTEXT!
83−else instance checkLabelExistsNotFound ::
134+else instance
84135 Fail
85136 ( Above
86137 (Text "Missing required dependency!")
87− (Above
88− (Text "")
89− (Above
90− (Text "The first missing field is: ")
91− (Above
92− (Quote label)
93− (Above
94− (Text "")
95− (Above
96− (Text "The layer requires: ")
97− (Above
98− (Quote requiredRow)
99− (Above
100− (Text "")
101− (Above
102− (Text "But you provided: ")
103− (Quote availableRow)
104− )
138+ ( Above
139+ (Text "")
140+ ( Above
141+ (Text "The first missing field is: ")
142+ ( Above
143+ (Quote label)
144+ ( Above
145+ (Text "")
146+ ( Above
147+ (Text "The layer requires: ")
148+ ( Above
149+ (Quote requiredRow)
150+ ( Above
151+ (Text "")
152+ ( Above
153+ (Text "But you provided: ")
154+ (Quote availableRow)
155+ )
156+ )
157+ )
158+ )
105159 )
106− )
107160 )
108− )
109161 )
110− )
111162 )
112163 ) =>
113164 CheckLabelExists label ty Nil requiredRow availableRow
114165
115−-- | Run a layer with a given context, with custom error if requirements aren't met
166+-- =============================================================================
167+-- Running layers
168+-- =============================================================================
169+
170+-- | Run a layer with a given context, discarding finalizers.
171+-- | Use `runScoped` instead if the layer has scoped resources.
116172runLayer
117173 :: forall req prov err available
118− . CheckAllProvided req available -- ← Custom error if dependencies missing!
174+ . CheckAllProvided req available
119175 => Record available
120176 -> OmLayer req prov err
121177 -> Om (Record available) err (Record prov)
122−runLayer _ctx layer = unsafeCastLayer layer
178+runLayer _ctx layer = unsafeCastLayer layer <#> \(prov /\ _) -> prov
123179 where
124− -- After CheckAllProvided succeeds, we know req ⊆ available at compile time
125− -- So this cast is safe - we're just telling the type system what it already proved
126− unsafeCastLayer :: OmLayer req prov err -> Om (Record available) err (Record prov)
180+ unsafeCastLayer :: OmLayer req prov err -> Om (Record available) err (Record prov /\ Finalizers)
127181 unsafeCastLayer = unsafeCoerce
128182
129−-- | The key function: combine two layers with automatic deduplication
130−-- Given two layers with potentially overlapping requirements,
131−-- produce a layer with deduplicated requirements
132−--
133−-- Uses expand to properly widen contexts and errors, then runs both layers!
183+-- | Run a fully-provided layer within a scope, passing the provisions to a
184+-- | callback. All finalizers are guaranteed to run in reverse order when the
185+-- | callback completes, whether by success, failure, or interruption.
186+runScoped
187+ :: forall prov a
188+ . OmLayer () prov ()
189+ -> (Record prov -> Aff a)
190+ -> Aff a
191+runScoped (OmLayer om) callback = generalBracket acquire conditions use
192+ where
193+ acquire = Om.runOm {} { exception: \_ -> pure (unsafeCoerce {} /\ mempty) } om
194+ conditions =
195+ { completed: \_ (_ /\ fins) -> runFinalizers fins
196+ , failed: \_ (_ /\ fins) -> runFinalizers fins
197+ , killed: \_ (_ /\ fins) -> runFinalizers fins
198+ }
199+ use (prov /\ _) = callback prov
200+
201+-- | Run finalizers in reverse order (LIFO).
202+runFinalizers :: Finalizers -> Aff Unit
203+runFinalizers (Finalizers fs) = for_ (Array.reverse fs) identity
204+
205+-- =============================================================================
206+-- Horizontal composition — combine two layers with automatic deduplication
207+-- =============================================================================
208+
134209combineRequirements
135210 :: forall req1 req2 prov1 prov2 err1 err2 provMerged provMergedRaw reqMerged reqDeduped _req1 _req2 _err1 _err2
136211 . Union req1 req2 reqMerged
137− => Nub reqMerged reqDeduped -- ← Automatic deduplication of requirements!
138− => Union req1 _req1 reqDeduped -- req1 is subset of reqDeduped
139− => Union req2 _req2 reqDeduped -- req2 is subset of reqDeduped
140− => Union err1 _err1 () -- Widen errors to empty
212+ => Nub reqMerged reqDeduped
213+ => Union req1 _req1 reqDeduped
214+ => Union req2 _req2 reqDeduped
215+ => Union err1 _err1 ()
141216 => Union err2 _err2 ()
142− => Union prov1 prov2 provMergedRaw -- Merge provisions
143− => Nub provMergedRaw provMerged -- Deduplicate provisions too!
144− => Keys req1 -- Needed for expand
145− => Keys req2 -- Needed for expand
217+ => Union prov1 prov2 provMergedRaw
218+ => Nub provMergedRaw provMerged
219+ => Keys req1
220+ => Keys req2
146221 => OmLayer req1 prov1 err1
147222 -> OmLayer req2 prov2 err2
148223 -> OmLayer reqDeduped provMerged ()
149−combineRequirements (OmLayer build1) (OmLayer build2) = makeLayer do
150− -- Widen both contexts and errors to allow them to run in the deduplicated context
151− -- expand allows an Om computation to run in a larger context with larger error row
152− rec1 <- Om.expand build1 -- Om req1 err1 -> Om reqDeduped ()
153− rec2 <- Om.expand build2 -- Om req2 err2 -> Om reqDeduped ()
154− -- Merge the results (Record.merge handles the Nub)
155− pure (Record.merge rec1 rec2)
156−
157−-- Example proof that this works:
158−-- If we have:
159−-- layer1 :: OmLayer (config :: Config) (logger :: Logger) ()
160−-- layer2 :: OmLayer (config :: Config) (database :: Database) ()
161−--
162−-- Then: combineRequirements layer1 layer2
163−-- Has type: OmLayer (config :: Config) prov3 err3
164−--
165−-- NOT: OmLayer (config :: Config, config :: Config) prov3 err3
166−--
167−-- The duplicated requirement (config, config) is automatically
168−-- deduplicated to just (config) by the Nub constraint!
224+combineRequirements (OmLayer build1) (OmLayer build2) = OmLayer do
225+ rec1 /\ fins1 <- Om.expand build1
226+ rec2 /\ fins2 <- Om.expand build2
227+ pure (Record.merge rec1 rec2 /\ (fins1 <> fins2))
169228
170229-- =============================================================================
171−-- Vertical Composition (ZLayer-style provide)
230+-- Vertical composition — feed output of one layer into input of another
172231-- =============================================================================
173232
174−-- | Vertical composition: feed output of one layer into input of another
175−-- | This is the >>> operator from ZIO ZLayer
176−-- |
177−-- | Example:
178−-- | postgresLive :: OmLayer (config :: Config) (postgres :: SQL) ()
179−-- | userRepoLive :: OmLayer (postgres :: SQL) (userRepo :: UserRepo) ()
180−-- |
181−-- | composed = userRepoLive `provide` postgresLive
182−-- | -- Result: OmLayer (config :: Config) (userRepo :: UserRepo) ()
183233provide
184234 :: forall req prov1 prov2 err1 err2 _req _prov1 _err1 _err2
185− . Union req _req req -- expand constraints
235+ . Union req _req req
186236 => Union prov1 _prov1 prov1
187237 => Union err1 _err1 ()
188238 => Union err2 _err2 ()
189239 => Keys req
190240 => Keys prov1
191− => OmLayer prov1 prov2 err2 -- layer that needs prov1, provides prov2
192− -> OmLayer req prov1 err1 -- layer that needs req, provides prov1
193− -> OmLayer req prov2 () -- composed: needs req, provides prov2
194−provide (OmLayer layer2) (OmLayer layer1) = makeLayer do
195− -- Run first layer to get prov1
196− prov1 <- Om.expand layer1
197−
198− -- Run second layer with prov1 as context
199− liftAff $ Om.runOm prov1
200− { exception: \_ -> pure (unsafeCoerce {}) }
241+ => OmLayer prov1 prov2 err2
242+ -> OmLayer req prov1 err1
243+ -> OmLayer req prov2 ()
244+provide (OmLayer layer2) (OmLayer layer1) = OmLayer do
245+ prov1 /\ fins1 <- Om.expand layer1
246+ prov2 /\ fins2 <- liftAff $ Om.runOm prov1
247+ { exception: \_ -> pure (unsafeCoerce {} /\ mempty) }
201248 (Om.expand layer2)
249+ pure (prov2 /\ (fins1 <> fins2))
202250
203251infixl 9 provide as >->
204−
205−-- TODO: More sophisticated composition operators
206−-- For now, use >>> for simple chains and combineRequirements for parallel deps
test/Test/Main.purs+126−22
@@ -3,16 +3,20 @@ module Test.Main where
33import Prelude
44
55import Data.Newtype (un)
6+import Data.Tuple.Nested ((/\))
67import Effect (Effect)
7−import Effect.Aff (launchAff_)
8+import Effect.Aff (launchAff_, throwError, try)
9+import Effect.Aff.Class (liftAff)
10+import Effect.Exception (error)
811import Effect.Class (liftEffect)
912import Effect.Class.Console as Console
1013import Effect.Ref as Ref
1114import Test.Spec (describe, it)
15+import Test.Spec.Assertions (shouldEqual)
1216import Test.Spec.Reporter (consoleReporter)
1317import Test.Spec.Runner (runSpec)
1418import Yoga.Om as Om
15−import Yoga.Om.Layer (OmLayer(..), makeLayer, combineRequirements)
19+import Yoga.Om.Layer (OmLayer(..), Finalizers, makeLayer, makeScopedLayer, bracketLayer, combineRequirements, runScoped, provide)
1620
1721-- Example types
1822type Config = { port :: Int, host :: String }
@@ -22,7 +26,7 @@ type Database = { query :: String -> Effect (Array String) }
2226main :: Effect Unit
2327main = launchAff_ $ runSpec [ consoleReporter ] do
2428 describe "OmLayer - Proof of Concept: Type-Level Deduplication" do
25−
29+
2630 it "proves Row.Nub deduplicates shared requirements at type level" do
2731 -- Define layers with dependencies
2832 let
@@ -31,22 +35,22 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
3135 loggerLayer = makeLayer do
3236 { config } <- Om.ask
3337 pure { logger: { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg } }
34−
38+
3539 -- Layer that also requires config
3640 databaseLayer :: OmLayer (config :: Config) (database :: Database) ()
3741 databaseLayer = makeLayer do
3842 { config } <- Om.ask
3943 pure { database: { query: \q -> pure [ "Result from " <> config.host ] } }
40−
44+
4145 -- The proof: This type-checks!
4246 -- combineRequirements takes two layers that each need (config :: Config)
4347 -- and produces a layer that needs (config :: Config) - NOT (config, config)!
4448 let
4549 -- The type annotation proves deduplication works:
46− _proofOfDeduplication
50+ _proofOfDeduplication
4751 :: OmLayer (config :: Config) _ _
4852 _proofOfDeduplication = combineRequirements loggerLayer databaseLayer
49−
53+
5054 Console.log "✓ PROOF OF CONCEPT SUCCESS!"
5155 Console.log " "
5256 Console.log " Key insight: Row.Nub automatically deduplicates at the type level"
@@ -65,13 +69,13 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
6569 Console.log " "
6670 Console.log " This is the foundation for ZLayer-style dependency injection"
6771 Console.log " where the type system automatically handles shared dependencies."
68−
72+
6973 pure unit
70−
74+
7175 it "proves deduplication at runtime via shared Ref" do
7276 -- Create a mutable reference to track accesses
7377 accessLog <- liftEffect $ Ref.new ""
74−
78+
7579 let
7680 -- Manually construct a combined layer that shows both "components"
7781 -- accessing the same shared context (with deduplicated requirements)
@@ -79,33 +83,33 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
7983 combinedLayer = makeLayer do
8084 -- Get the shared context - note the type signature shows it's deduplicated!
8185 { config, accessLog: log } <- Om.ask
82−
86+
8387 -- "Logger component" accesses config and logs it
8488 liftEffect $ Ref.modify_ (_ <> "config-accessed-by-logger ") log
8589 let logger = { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg }
86−
90+
8791 -- "Database component" also accesses config and logs it
8892 -- CRITICAL: They're both accessing the SAME context record!
8993 liftEffect $ Ref.modify_ (_ <> "config-accessed-by-database ") log
9094 let database = { query: \q -> pure [ "Result from " <> config.host ] }
91−
95+
9296 pure { logger, database }
93−
97+
9498 -- Run with the context
95− result <- un OmLayer combinedLayer
96− # Om.runOm
99+ result /\ _ <- un OmLayer combinedLayer
100+ # Om.runOm
97101 { config: { port: 5432, host: "localhost" }
98102 , accessLog: accessLog
99− }
100− { exception: \_ -> pure { logger: { log: \_ -> pure unit }, database: { query: \_ -> pure [] } } }
101−
103+ }
104+ { exception: \_ -> pure ({ logger: { log: \_ -> pure unit }, database: { query: \_ -> pure [] } } /\ (mempty :: Finalizers)) }
105+
102106 -- Check the access log
103107 finalLog <- liftEffect $ Ref.read accessLog
104−
108+
105109 -- Use the resulting services to prove they work
106110 liftEffect $ result.logger.log "Testing logger from combined layer"
107111 queryResult <- liftEffect $ result.database.query "SELECT *"
108−
112+
109113 -- Both "layers" ran and accessed config via the same context
110114 Console.log ""
111115 Console.log "✓ RUNTIME DEDUPLICATION PROOF:"
@@ -119,5 +123,105 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
119123 Console.log " "
120124 Console.log " At runtime, both components read from the same context record,"
121125 Console.log " and both wrote to the same Ref, proving deduplication works!"
122−
126+
123127 pure unit
128+
129+ describe "Scoped Layers" do
130+
131+ it "runs finalizers in reverse order on success" do
132+ log <- liftEffect $ Ref.new []
133+ let
134+ layerA :: OmLayer () (a :: String) ()
135+ layerA = makeScopedLayer
136+ (pure { a: "A" })
137+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-A" ]) log)
138+
139+ layerB :: OmLayer () (b :: String) ()
140+ layerB = makeScopedLayer
141+ (pure { b: "B" })
142+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-B" ]) log)
143+
144+ layerC :: OmLayer () (c :: String) ()
145+ layerC = makeScopedLayer
146+ (pure { c: "C" })
147+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-C" ]) log)
148+
149+ layerBC :: OmLayer () (b :: String, c :: String) ()
150+ layerBC = combineRequirements layerB layerC
151+
152+ combined = combineRequirements layerA layerBC
153+
154+ liftAff $ runScoped combined \_ -> pure unit
155+ finalLog <- liftEffect $ Ref.read log
156+ finalLog `shouldEqual` [ "release-C", "release-B", "release-A" ]
157+
158+ it "runs finalizers when the callback throws" do
159+ log <- liftEffect $ Ref.new []
160+ let
161+ layer = makeScopedLayer
162+ (pure { value: "acquired" })
163+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "released" ]) log)
164+
165+ -- The callback throws, but finalizers should still run
166+ _ <- liftAff $ runScoped layer (\_ -> throwError (error "callback failed"))
167+ # try
168+ finalLog <- liftEffect $ Ref.read log
169+ finalLog `shouldEqual` [ "released" ]
170+
171+ it "threads finalizers through vertical composition (provide)" do
172+ log <- liftEffect $ Ref.new []
173+ let
174+ baseLayer :: OmLayer () (base :: String) ()
175+ baseLayer = makeScopedLayer
176+ (pure { base: "base-value" })
177+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-base" ]) log)
178+
179+ upperLayer :: OmLayer (base :: String) (upper :: String) ()
180+ upperLayer = makeScopedLayer
181+ ( do
182+ { base } <- Om.ask
183+ pure { upper: base <> "-extended" }
184+ )
185+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-upper" ]) log)
186+
187+ composed = upperLayer `provide` baseLayer
188+
189+ result <- liftAff $ runScoped composed \r -> pure r.upper
190+ result `shouldEqual` "base-value-extended"
191+ finalLog <- liftEffect $ Ref.read log
192+ finalLog `shouldEqual` [ "release-upper", "release-base" ]
193+
194+ it "works with bracketLayer" do
195+ log <- liftEffect $ Ref.new []
196+ let
197+ layer = bracketLayer
198+ ( do
199+ liftEffect $ Ref.modify_ (_ <> [ "acquire" ]) log
200+ pure 42
201+ )
202+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release" ]) log)
203+ (\n -> pure { value: n })
204+
205+ result <- liftAff $ runScoped layer \r -> pure r.value
206+ result `shouldEqual` 42
207+ finalLog <- liftEffect $ Ref.read log
208+ finalLog `shouldEqual` [ "acquire", "release" ]
209+
210+ it "combines scoped and non-scoped layers" do
211+ log <- liftEffect $ Ref.new []
212+ let
213+ scopedLayer :: OmLayer () (scoped :: String) ()
214+ scopedLayer = makeScopedLayer
215+ (pure { scoped: "yes" })
216+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-scoped" ]) log)
217+
218+ plainLayer :: OmLayer () (plain :: String) ()
219+ plainLayer = makeLayer (pure { plain: "no-finalizer" })
220+
221+ combined = combineRequirements scopedLayer plainLayer
222+
223+ result <- liftAff $ runScoped combined \r -> pure r
224+ result.scoped `shouldEqual` "yes"
225+ result.plain `shouldEqual` "no-finalizer"
226+ finalLog <- liftEffect $ Ref.read log
227+ finalLog `shouldEqual` [ "release-scoped" ]