Skip to content

Conversation

nicklasl
Copy link
Member

Summary

Implements sticky resolve functionality for the JavaScript OpenFeature provider to ensure consistent variant assignments even when user context changes or new assignments are paused. This includes support for both local storage (MaterializationRepository) and remote fallback (ResolverFallback) strategies.

Changes

Core Implementation

  • Protocol Buffers: Added sticky resolve messages (ResolveWithStickyRequest, ResolveWithStickyResponse, MaterializationInfo) to api.proto
  • Strategy Interfaces: Created StickyResolveStrategy with two implementations:
    • MaterializationRepository: For local storage of materialized assignments (Redis, DB, etc.)
    • ResolverFallback: For remote API fallback when materializations are missing
  • Type Guards: Added isResolverFallback() and isMaterializationRepository() for runtime type checking

Provider Integration

  • Always-on Sticky Resolve: Updated ConfidenceServerProviderLocal to always use sticky resolve
  • Recursive Resolution: Implemented resolveWithStickyInternal() matching Java SDK pattern
  • Default Strategy: RemoteResolverFallback is now the default, simplifying setup
  • Fire-and-Forget Storage: Non-blocking materialization updates for optimal performance

Remote Fallback

  • RemoteResolverFallback: New class that delegates to Confidence API (https://resolver.confidence.dev/v1/flags:resolve)
  • Configurable: Supports custom base URL and fetch implementation
  • Automatic TTL: Server-side 90-day TTL for materializations

Utilities

  • materializationUtils.ts: Extracted utility functions:
    • handleMissingMaterializations(): Bulk loads materializations from repository
    • storeUpdates(): Fire-and-forget storage of new assignments

Test Coverage

  • ✅ 14 tests for StickyResolveStrategy (type guards, interface contracts)
  • ✅ 16 tests for RemoteResolverFallback (remote API integration)
  • ✅ 11 tests for sticky resolve in ConfidenceServerProviderLocal
  • Total: 55 tests passing

API Changes

New Exports

// Strategy interfaces
export type { StickyResolveStrategy, MaterializationRepository, ResolverFallback }

// Default implementation
export { RemoteResolverFallback }
export type { RemoteResolverFallbackOptions }

// Proto types
export type { MaterializationInfo, ResolveFlagsRequest, ResolveFlagsResponse }

Provider Options

interface ProviderOptions {
  // ... existing options
  stickyResolveStrategy?: StickyResolveStrategy; // Defaults to RemoteResolverFallback
}

Usage Examples

Default (Remote Fallback)

const provider = createConfidenceServerProvider({
  flagClientSecret: 'client-secret',
  apiClientId: 'api-id',
  apiClientSecret: 'api-secret'
  // RemoteResolverFallback used automatically
});

Custom Repository

class RedisMaterializationRepository implements MaterializationRepository {
  async loadMaterializedAssignmentsForUnit(unit: string, materialization: string) {
    const data = await this.redis.get(`unit:${unit}`);
    return new Map(Object.entries(JSON.parse(data || '{}')));
  }

  async storeAssignment(unit: string, assignments: Map<string, MaterializationInfo>) {
    await this.redis.set(`unit:${unit}`, JSON.stringify(Object.fromEntries(assignments)));
  }

  close() { this.redis.disconnect(); }
}

const provider = createConfidenceServerProvider({
  flagClientSecret: 'client-secret',
  apiClientId: 'api-id',
  apiClientSecret: 'api-secret',
  stickyResolveStrategy: new RedisMaterializationRepository(redis)
});

Implementation Notes

  • Matches Java SDK implementation for consistency
  • Fire-and-forget storage prevents blocking flag resolution path
  • Bulk loading of materializations for efficiency
  • Type-safe with no non-null assertions
  • Fully backward compatible (sticky resolve is transparent to users)

Test Plan

  • All existing tests still pass
  • New strategy type guard tests
  • RemoteResolverFallback unit tests
  • Provider integration tests with both strategies
  • Missing materialization retry flow
  • Update storage flow

🤖 Generated with Claude Code

nicklasl and others added 5 commits October 10, 2025 13:45
Implements sticky resolve functionality to ensure consistent variant
assignments even when user context changes or new assignments are paused.

Key changes:
- Add proto definitions for ResolveWithStickyRequest/Response
- Add StickyResolveStrategy interface with MaterializationRepository and
  ResolverFallback implementations
- Refactor evaluate() to always use sticky resolve via
  resolveWithStickyInternal()
- Add materialization utility functions for loading/storing assignments
- Make evaluate() async to support async storage operations
- Add comprehensive test coverage (25 tests)

The implementation matches the Java SDK pattern, with fire-and-forget
storage updates and recursive retry logic for missing materializations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implements RemoteResolverFallback which delegates to the remote Confidence
API when materializations are missing. This is now the default strategy,
simplifying setup for users while maintaining extensibility.

Changes:
- Add RemoteResolverFallback class with remote API resolution
- Default stickyResolveStrategy to RemoteResolverFallback
- Remove non-null assertions for stickyResolveStrategy
- Export RemoteResolverFallback in public API
- Add comprehensive tests (16 tests, all passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
} from "./proto/api"

export interface LocalResolver {
resolveFlags(request: ResolveFlagsRequest): ResolveFlagsResponse
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove resolveFlags() since it is never used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There's a lot we should remove from the guest interface that we never use.

nicklasl and others added 7 commits October 14, 2025 18:46
…ation

Refactor FileBackedMaterializationRepo to use InMemoryMaterializationRepo as backing store, reading from file only on init and writing only on close.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove resolveFlags method from LocalResolver interface and WasmResolver implementation. Update all tests to use resolveWithSticky instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove ResolverFallback interface from exports as users should use the concrete RemoteResolverFallback implementation instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove ResolveFlagsRequest and ResolveFlagsResponse type exports as they are only used internally by ResolverFallback which is no longer public.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Change `stickyResolveStrategy` to `materializationRepository` parameter accepting either MaterializationRepository or RemoteResolverFallback. Remove StickyResolveStrategy from public exports.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Simplify API by removing intermediate interfaces:
- Remove StickyResolveStrategy base interface
- Remove ResolverFallback interface and type guards
- Make MaterializationRepository standalone with close() method
- Rename StickyResolveStrategy.ts to MaterializationRepository.ts
- Update ConfidenceServerProviderLocal to use optional MaterializationRepository
- RemoteResolverFallback is now created internally when no repository provided

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants