@@ -280,7 +280,7 @@ func (p *LocalResolverProvider) ObjectEvaluation(
280280 }
281281
282282 // Resolve flags with sticky support
283- response , err := p .resolverAPI .ResolveWithSticky (ctx , stickyRequest )
283+ stickyResponse , err := p .resolverAPI .ResolveWithSticky (ctx , stickyRequest )
284284 if err != nil {
285285 p .logger .Error ("Failed to resolve flag" , "flag" , flagPath , "error" , err )
286286 return openfeature.InterfaceResolutionDetail {
@@ -292,6 +292,19 @@ func (p *LocalResolverProvider) ObjectEvaluation(
292292 }
293293 }
294294
295+ // Handle the sticky response and extract the actual resolve response
296+ response , err := p .handleStickyResponse (ctx , stickyRequest , stickyResponse )
297+ if err != nil {
298+ p .logger .Error ("Failed to handle sticky response" , "flag" , flagPath , "error" , err )
299+ return openfeature.InterfaceResolutionDetail {
300+ Value : defaultValue ,
301+ ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
302+ Reason : openfeature .ErrorReason ,
303+ ResolutionError : openfeature .NewGeneralResolutionError (fmt .Sprintf ("resolve failed: %v" , err )),
304+ },
305+ }
306+ }
307+
295308 // Check if flag was found
296309 if len (response .ResolvedFlags ) == 0 {
297310 p .logger .Info ("No active flag was found" , "flag" , flagPath )
@@ -448,6 +461,145 @@ func (p *LocalResolverProvider) Shutdown() {
448461 }
449462}
450463
464+ // handleStickyResponse processes the sticky response and returns the actual resolve response
465+ func (p * LocalResolverProvider ) handleStickyResponse (
466+ ctx context.Context ,
467+ request * resolver.ResolveWithStickyRequest ,
468+ stickyResponse * resolver.ResolveWithStickyResponse ,
469+ ) (* resolver.ResolveFlagsResponse , error ) {
470+ // Handle the response based on result type
471+ switch result := stickyResponse .ResolveResult .(type ) {
472+ case * resolver.ResolveWithStickyResponse_Success_ :
473+ success := result .Success
474+ // Store updates if present and we have a MaterializationRepository
475+ if len (success .GetUpdates ()) > 0 {
476+ p .storeUpdates (ctx , success .GetUpdates ())
477+ }
478+ return success .Response , nil
479+
480+ case * resolver.ResolveWithStickyResponse_MissingMaterializations_ :
481+ missingMaterializations := result .MissingMaterializations
482+
483+ // Check for ResolverFallback first - return early if so
484+ if fallback , ok := p .stickyResolveStrategy .(ResolverFallback ); ok {
485+ return fallback .Resolve (ctx , request .GetResolveRequest ())
486+ }
487+
488+ // Handle MaterializationRepository case
489+ if repo , ok := p .stickyResolveStrategy .(MaterializationRepository ); ok {
490+ updatedRequest , err := p .handleMissingMaterializations (ctx , request , missingMaterializations .GetItems (), repo )
491+ if err != nil {
492+ return nil , fmt .Errorf ("failed to handle missing materializations: %w" , err )
493+ }
494+ // Retry with the updated request
495+ retryResponse , err := p .resolverAPI .ResolveWithSticky (ctx , updatedRequest )
496+ if err != nil {
497+ return nil , err
498+ }
499+ // Recursively handle the response (in case there are more missing materializations)
500+ return p .handleStickyResponse (ctx , updatedRequest , retryResponse )
501+ }
502+
503+ // If no strategy is configured, return an error
504+ if p .stickyResolveStrategy == nil {
505+ return nil , fmt .Errorf ("missing materializations and no sticky resolve strategy configured" )
506+ }
507+
508+ return nil , fmt .Errorf ("unknown sticky resolve strategy type: %T" , p .stickyResolveStrategy )
509+
510+ default :
511+ return nil , fmt .Errorf ("unexpected resolve result type: %T" , stickyResponse .ResolveResult )
512+ }
513+ }
514+
515+ // storeUpdates stores materialization updates asynchronously if we have a MaterializationRepository
516+ func (p * LocalResolverProvider ) storeUpdates (ctx context.Context , updates []* resolver.ResolveWithStickyResponse_MaterializationUpdate ) {
517+ repo , ok := p .stickyResolveStrategy .(MaterializationRepository )
518+ if ! ok {
519+ return
520+ }
521+
522+ // Store updates asynchronously
523+ go func () {
524+ // Group updates by unit
525+ updatesByUnit := make (map [string ][]* resolver.ResolveWithStickyResponse_MaterializationUpdate )
526+ for _ , update := range updates {
527+ updatesByUnit [update .GetUnit ()] = append (updatesByUnit [update .GetUnit ()], update )
528+ }
529+
530+ // Store assignments for each unit
531+ for unit , unitUpdates := range updatesByUnit {
532+ assignments := make (map [string ]* MaterializationInfo )
533+ for _ , update := range unitUpdates {
534+ ruleToVariant := map [string ]string {update .GetRule (): update .GetVariant ()}
535+ assignments [update .GetWriteMaterialization ()] = & MaterializationInfo {
536+ UnitInMaterialization : true ,
537+ RuleToVariant : ruleToVariant ,
538+ }
539+ }
540+
541+ if err := repo .StoreAssignment (ctx , unit , assignments ); err != nil {
542+ p .logger .Error ("Failed to store materialization updates" ,
543+ "unit" , unit ,
544+ "error" , err )
545+ }
546+ }
547+ }()
548+ }
549+
550+ // handleMissingMaterializations loads missing materializations from the repository
551+ // and returns an updated request with the materializations added
552+ func (p * LocalResolverProvider ) handleMissingMaterializations (
553+ ctx context.Context ,
554+ request * resolver.ResolveWithStickyRequest ,
555+ missingItems []* resolver.ResolveWithStickyResponse_MissingMaterializationItem ,
556+ repo MaterializationRepository ,
557+ ) (* resolver.ResolveWithStickyRequest , error ) {
558+ // Group missing items by unit for efficient loading
559+ missingByUnit := make (map [string ][]* resolver.ResolveWithStickyResponse_MissingMaterializationItem )
560+ for _ , item := range missingItems {
561+ missingByUnit [item .GetUnit ()] = append (missingByUnit [item .GetUnit ()], item )
562+ }
563+
564+ // Create the materializations per unit map
565+ materializationsPerUnit := make (map [string ]* resolver.MaterializationMap )
566+
567+ // Copy existing materializations
568+ for k , v := range request .GetMaterializationsPerUnit () {
569+ materializationsPerUnit [k ] = v
570+ }
571+
572+ // Load materialized assignments for all missing units
573+ for unit , items := range missingByUnit {
574+ for _ , item := range items {
575+ loadedAssignments , err := repo .LoadMaterializedAssignmentsForUnit (ctx , unit , item .GetReadMaterialization ())
576+ if err != nil {
577+ return nil , fmt .Errorf ("failed to load materializations for unit %s: %w" , unit , err )
578+ }
579+
580+ // Ensure the map exists for this unit
581+ if materializationsPerUnit [unit ] == nil {
582+ materializationsPerUnit [unit ] = & resolver.MaterializationMap {
583+ InfoMap : make (map [string ]* resolver.MaterializationInfo ),
584+ }
585+ }
586+
587+ // Add loaded assignments to the materialization map
588+ for name , info := range loadedAssignments {
589+ materializationsPerUnit [unit ].InfoMap [name ] = info .ToProto ()
590+ }
591+ }
592+ }
593+
594+ // Create a new request with the updated materializations
595+ return & resolver.ResolveWithStickyRequest {
596+ ResolveRequest : request .GetResolveRequest (),
597+ MaterializationsPerUnit : materializationsPerUnit ,
598+ FailFastOnSticky : request .GetFailFastOnSticky (),
599+ NotProcessSticky : request .GetNotProcessSticky (),
600+ }, nil
601+ }
602+
451603// startScheduledTasks starts the background tasks for state fetching and log polling
452604func (p * LocalResolverProvider ) startScheduledTasks (parentCtx context.Context ) {
453605 ctx , cancel := context .WithCancel (parentCtx )
0 commit comments