@@ -2,10 +2,11 @@
22-- @inline export runLayer arity=2
33module Yoga.Om.Layer
44 ( OmLayer
5− , Finalizers
5+ , Scope
66 , makeLayer
77 , makeScopedLayer
88 , bracketLayer
9+ , acquireRelease
910 , runLayer
1011 , runScoped
1112 , withScoped
@@ -21,8 +22,12 @@ import Prelude
2122
2223import Data.Array as Array
2324import Data.Foldable (for_)
24−import Effect.Aff (Aff, generalBracket)
25+import Effect (Effect)
26+import Effect.Aff (Aff, bracket)
2527import Effect.Aff.Class (liftAff)
28+import Effect.Class (liftEffect)
29+import Effect.Ref (Ref)
30+import Effect.Ref as Ref
2631import Prim.Row (class Nub, class Union)
2732import Prim.RowList (class RowToList, Cons, Nil, RowList)
2833import Prim.TypeError (class Fail, Above, Quote, Text)
@@ -34,63 +39,79 @@ import Yoga.Om (Om)
3439import Yoga.Om as Om
3540
3641-- =============================================================================
37−-- Finalizers — guaranteed cleanup actions collected during layer construction
42+-- Scope — a first-class representation of resource lifetime
3843-- =============================================================================
3944
40−-- | Cleanup actions to run when a scope closes. Stored in construction order;
41−-- | executed in reverse (LIFO) by `runScoped`.
42−newtype Finalizers = Finalizers (Array (Aff Unit))
45+-- | A mutable collection of finalizers representing the lifetime of resources.
46+-- | Finalizers are registered via `acquireRelease` and run in reverse order
47+-- | when the scope is closed.
48+newtype Scope = Scope (Ref (Array (Aff Unit)))
4349
44−instance Semigroup Finalizers where
45− append (Finalizers a) (Finalizers b) = Finalizers (a <> b)
50+-- | Create a fresh, empty scope.
51+newScope :: Effect Scope
52+newScope = Scope <$> Ref.new []
4653
47−instance Monoid Finalizers where
48− mempty = Finalizers []
54+-- | Register a finalizer to run when this scope closes.
55+addFinalizer :: Scope -> Aff Unit -> Effect Unit
56+addFinalizer (Scope ref) fin = Ref.modify_ (_ <> [ fin ]) ref
57+
58+-- | Close the scope, running all registered finalizers in reverse order.
59+closeScope :: Scope -> Aff Unit
60+closeScope (Scope ref) = do
61+ fins <- liftEffect $ Ref.read ref
62+ for_ (Array.reverse fins) identity
4963
5064-- =============================================================================
51−-- OmLayer — a layer that requires dependencies, provides services, and
52−-- carries finalizers for resource cleanup
65+-- OmLayer
5366-- =============================================================================
5467
55−type Built prov = { provisions :: Record prov, finalizers :: Finalizers }
56−
57−-- | A layer that requires dependencies (req), provides services (prov),
58−-- | and may carry finalizers for scoped resource management.
59−data OmLayer req prov err = OmLayer (Om (Record req) err (Built prov))
68+-- | A layer that requires dependencies (req) and provides services (prov).
69+data OmLayer req prov err = OmLayer (Om (Record req) err (Record prov))
6070
6171-- =============================================================================
6272-- Layer constructors
6373-- =============================================================================
6474
65−-- | Create a layer from an Om computation (no finalizers).
75+-- | Create a layer from an Om computation.
6676makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
67−makeLayer om = OmLayer (om <#> \provisions -> { provisions, finalizers: mempty })
77+makeLayer = OmLayer
6878
6979-- | Create a scoped layer with acquire/release semantics.
70−-- | The release function is guaranteed to run when `runScoped` closes the scope.
80+-- | The release function runs when the enclosing scope closes.
81+-- | The layer requires `scope :: Scope` in its context.
7182makeScopedLayer
7283 :: forall req prov err
73− . Om (Record req) err (Record prov)
84+ . Om { scope :: Scope | req } err (Record prov)
7485 -> (Record prov -> Aff Unit)
75− -> OmLayer req prov err
76−makeScopedLayer acquire release = OmLayer do
77− provisions <- acquire
78− let finalizers = Finalizers [ release provisions ]
79− pure { provisions, finalizers }
86+ -> OmLayer (scope :: Scope | req) prov err
87+makeScopedLayer acquire release = makeLayer do
88+ acquireRelease acquire release
8089
81−-- | Bracket-style scoped layer: acquire a resource, build provisions from it,
82−-- | and register a finalizer for the resource.
90+-- | Bracket-style scoped layer: acquire a resource, register its release,
91+-- | then build provisions from it.
8392bracketLayer
8493 :: forall req prov resource err
85− . Om (Record req) err resource
94+ . Om { scope :: Scope | req } err resource
8695 -> (resource -> Aff Unit)
87− -> (resource -> Om (Record req) err (Record prov))
88− -> OmLayer req prov err
89−bracketLayer acquire release use = OmLayer do
90− resource <- acquire
91− provisions <- use resource
92− let finalizers = Finalizers [ release resource ]
93− pure { provisions, finalizers }
96+ -> (resource -> Om { scope :: Scope | req } err (Record prov))
97+ -> OmLayer (scope :: Scope | req) prov err
98+bracketLayer acquire release use = makeLayer do
99+ resource <- acquireRelease acquire (\r -> release r)
100+ use resource
101+
102+-- | Acquire a resource and register its release with the current scope.
103+-- | This is the core scoped resource primitive — the PureScript equivalent
104+-- | of ZIO's `ZIO.acquireRelease`.
105+acquireRelease
106+ :: forall ctx err a
107+ . Om { scope :: Scope | ctx } err a
108+ -> (a -> Aff Unit)
109+ -> Om { scope :: Scope | ctx } err a
110+acquireRelease acquire release = do
111+ a <- acquire
112+ { scope } <- Om.ask
113+ addFinalizer scope (release a) # liftEffect
114+ pure a
94115
95116-- =============================================================================
96117-- Custom Type Errors for Missing Dependencies
@@ -166,49 +187,43 @@ else instance
166187-- Running layers
167188-- =============================================================================
168189
169−-- | Run a layer with a given context, discarding finalizers.
170−-- | Use `runScoped` instead if the layer has scoped resources.
190+-- | Run a layer with a given context, with custom error if requirements aren't met.
171191runLayer
172192 :: forall req prov err available
173193 . CheckAllProvided req available
174194 => Record available
175195 -> OmLayer req prov err
176196 -> Om (Record available) err (Record prov)
177−runLayer _ctx (OmLayer om) = widenCtx om <#> _.provisions
197+runLayer _ctx (OmLayer om) = widenCtx om
178198 where
179− widenCtx :: Om (Record req) err (Built prov) -> Om (Record available) err (Built prov)
199+ widenCtx :: Om (Record req) err (Record prov) -> Om (Record available) err (Record prov)
180200 widenCtx = unsafeCoerce
181201
182−-- | Build a fully-provided layer, run finalizers, return the provisions.
183−-- | Useful when you just need the built services.
202+-- | Build a fully-provided scoped layer, return the provisions.
203+-- | All finalizers run after the provisions are returned.
184204runScoped
185205 :: forall prov
186− . OmLayer () prov ()
206+ . OmLayer (scope :: Scope) prov ()
187207 -> Aff (Record prov)
188208runScoped layer = withScoped layer pure
189209
190−-- | Build a fully-provided layer and pass the provisions to a callback.
191−-- | All finalizers run in reverse order when the callback completes,
192−-- | whether by success, failure, or interruption.
210+-- | Build a fully-provided scoped layer and pass the provisions to a callback.
211+-- | A fresh `Scope` is created and closed when the callback completes,
212+-- | running all finalizers in reverse order — whether by success, failure,
213+-- | or interruption.
193214withScoped
194215 :: forall prov a
195− . OmLayer () prov ()
216+ . OmLayer (scope :: Scope) prov ()
196217 -> (Record prov -> Aff a)
197218 -> Aff a
198−withScoped (OmLayer om) callback = generalBracket acquire conditions use
219+withScoped (OmLayer om) callback = bracket acquire release use
199220 where
200− acquire = Om.runOm {} errorHandlers om
201− errorHandlers = { exception: \_ -> pure { provisions: unsafeCoerce {}, finalizers: mempty } }
202− conditions =
203− { completed: \_ built -> runFinalizers built.finalizers
204− , failed: \_ built -> runFinalizers built.finalizers
205− , killed: \_ built -> runFinalizers built.finalizers
206− }
207− use built = callback built.provisions
208−
209−-- | Run finalizers in reverse order (LIFO).
210−runFinalizers :: Finalizers -> Aff Unit
211−runFinalizers (Finalizers fs) = for_ (Array.reverse fs) identity
221+ acquire = do
222+ scope <- newScope # liftEffect
223+ prov <- Om.runOm { scope } { exception: \_ -> pure (unsafeCoerce {}) } om
224+ pure { scope, prov }
225+ release { scope } = closeScope scope
226+ use { prov } = callback prov
212227
213228-- =============================================================================
214229-- Horizontal composition — combine two layers with automatic deduplication
@@ -230,34 +245,31 @@ combineRequirements
230245 -> OmLayer req2 prov2 err2
231246 -> OmLayer reqDeduped provMerged ()
232247combineRequirements (OmLayer build1) (OmLayer build2) = OmLayer do
233− built1 <- Om.expand build1
234− built2 <- Om.expand build2
235− let provisions = Record.merge built1.provisions built2.provisions
236− let finalizers = built1.finalizers <> built2.finalizers
237− pure { provisions, finalizers }
248+ rec1 <- Om.expand build1
249+ rec2 <- Om.expand build2
250+ pure (Record.merge rec1 rec2)
238251
239252-- =============================================================================
240253-- Vertical composition — feed output of one layer into input of another
241254-- =============================================================================
242255
243256provide
244− :: forall req prov1 prov2 err1 err2 _req _prov1 _err1 _err2
257+ :: forall req prov1 prov2 err1 err2 req2 reqOut _req _prov1 _err1 _err2
245258 . Union req _req req
246259 => Union prov1 _prov1 prov1
247260 => Union err1 _err1 ()
248261 => Union err2 _err2 ()
262+ => Union prov1 req reqOut
263+ => Nub reqOut reqOut
264+ => Union req2 _prov1 reqOut
249265 => Keys req
250266 => Keys prov1
251− => OmLayer prov1 prov2 err2
267+ => Keys req2
268+ => OmLayer req2 prov2 err2
252269 -> OmLayer req prov1 err1
253270 -> OmLayer req prov2 ()
254271provide (OmLayer layer2) (OmLayer layer1) = OmLayer do
255− built1 <- Om.expand layer1
256− built2 <- runLayer2 built1.provisions
257− let finalizers = built1.finalizers <> built2.finalizers
258− pure { provisions: built2.provisions, finalizers }
259− where
260− runLayer2 ctx = liftAff $ Om.runOm ctx errorHandlers (Om.expand layer2)
261− errorHandlers = { exception: \_ -> pure { provisions: unsafeCoerce {}, finalizers: mempty } }
272+ prov1 <- Om.expand layer1
273+ Om.expand layer2 # Om.widenCtx prov1
262274
263275infixl 9 provide as >->