Skip to content

Commit 985a5eb

Browse files
nicklaslclaude
andcommitted
refactor: use remote resolver fallback for sticky assignments
Remove MaterializationRepository from public API for now: - Removed materializationRepository option from ProviderOptions - Updated provider to always use remote resolver fallback for sticky assignments - Materializations stored on Confidence servers with 90-day TTL - Marked MaterializationRepository interface and utilities as WIP Documentation updates: - Updated README to explain remote fallback approach - Added note about upcoming custom storage feature - Marked MAT_REPO_EXAMPLES.md as work in progress - Updated tests to reflect remote fallback behavior The MaterializationRepository feature will be added in a future release to allow users to connect their own storage (Redis, database, etc.). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ed181e7 commit 985a5eb

File tree

6 files changed

+73
-80
lines changed

6 files changed

+73
-80
lines changed

openfeature-provider/js/MAT_REPO_EXAMPLES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# MaterializationRepository Examples
22

3+
> **⚠️ WORK IN PROGRESS**
4+
> This feature is currently in development and not yet available in the public API.
5+
> The `MaterializationRepository` interface will allow users to provide their own storage backend for sticky assignment materializations.
6+
37
This document provides implementation examples for custom `MaterializationRepository` implementations.
48

59
## Table of Contents

openfeature-provider/js/README.md

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ await provider.onClose();
7171
- `initializeTimeout` (number, optional): Max ms to wait for initial state fetch. Defaults to 30_000.
7272
- `flushInterval` (number, optional): Interval in ms for sending evaluation logs. Defaults to 10_000.
7373
- `fetch` (optional): Custom `fetch` implementation. Required for Node < 18; for Node 18+ you can omit.
74-
- `materializationRepository` (optional): Custom storage for sticky assignments. See [Sticky Assignments](#sticky-assignments) below.
7574

7675
The provider periodically:
7776
- Refreshes resolver state (default every 30s)
@@ -81,7 +80,7 @@ The provider periodically:
8180

8281
## Sticky Assignments
8382

84-
Confidence supports "sticky" flag assignments to ensure users receive consistent variant assignments even when their context changes or flag configurations are updated.
83+
Confidence supports "sticky" flag assignments to ensure users receive consistent variant assignments even when their context changes or flag configurations are updated.
8584

8685
### How it works
8786

@@ -90,66 +89,38 @@ When a flag is evaluated for a user, Confidence creates a "materialization" - a
9089
- The flag's targeting rules are modified
9190
- New assignments are paused
9291

93-
### Default behavior (no repository)
92+
### Implementation
9493

95-
If you don't provide a `materializationRepository`, the provider automatically falls back to resolve with our cloud resolvers:
96-
- Materializations are stored on Confidence servers with a 90-day TTL
94+
The provider uses a **remote resolver fallback** for sticky assignments:
95+
- First, the local WASM resolver attempts to resolve the flag
96+
- If sticky assignment data is needed, the provider makes a network call to Confidence's cloud resolvers
97+
- Materializations are stored on Confidence servers with a **90-day TTL** (automatically renewed on access)
9798
- No local storage or database setup required
98-
- Best for most use cases
9999

100100
```ts
101101
const provider = createConfidenceServerProvider({
102102
flagClientSecret: process.env.CONFIDENCE_FLAG_CLIENT_SECRET!,
103103
apiClientId: process.env.CONFIDENCE_API_CLIENT_ID!,
104104
apiClientSecret: process.env.CONFIDENCE_API_CLIENT_SECRET!,
105-
// materializationRepository is optional - uses remote storage by default
106105
});
107-
```
108106

109-
### Custom storage with MaterializationRepository
107+
// Sticky assignments work automatically via remote fallback
108+
const client = OpenFeature.getClient();
109+
const value = await client.getBooleanValue('my-flag', false, {
110+
targetingKey: 'user-123'
111+
});
112+
```
110113

111-
For advanced use cases where you want full control over materialization storage (e.g., Redis, database, file system), implement the `MaterializationRepository` interface:
114+
### Benefits
112115

113-
```ts
114-
interface MaterializationRepository {
115-
/**
116-
* Load ALL stored materialization assignments for a targeting unit.
117-
*
118-
* @param unit - The targeting key (e.g., user ID, session ID)
119-
* @param materialization - The materialization ID being requested (for context)
120-
* @returns Map of materialization ID to MaterializationInfo for this unit
121-
*/
122-
loadMaterializedAssignmentsForUnit(
123-
unit: string,
124-
materialization: string
125-
): Promise<Map<string, MaterializationInfo>>;
126-
127-
/**
128-
* Store materialization assignments for a targeting unit.
129-
*
130-
* @param unit - The targeting key (e.g., user ID, session ID)
131-
* @param assignments - Map of materialization ID to MaterializationInfo
132-
*/
133-
storeAssignment(
134-
unit: string,
135-
assignments: Map<string, MaterializationInfo>
136-
): Promise<void>;
137-
138-
/**
139-
* Close and cleanup any resources used by this repository.
140-
*/
141-
close(): void | Promise<void>;
142-
}
143-
```
116+
- **Zero configuration**: Works out of the box with no additional setup
117+
- **Managed storage**: Confidence handles all storage, TTL, and consistency
118+
- **Automatic renewal**: TTL is refreshed on each access
119+
- **Global availability**: Materializations are available across all your services
144120

145-
**Key points:**
146-
- `loadMaterializedAssignmentsForUnit` should return ALL materializations for the given unit, not just the one requested
147-
- This allows efficient bulk loading from your storage system
148-
- The provider handles caching and coordination internally
121+
### Coming Soon: Custom Materialization Storage
149122

150-
See [MAT_REPO_EXAMPLES.md](./MAT_REPO_EXAMPLES.md) for complete implementation examples including:
151-
- In-memory repository for testing
152-
- File-backed repository for persistent storage
123+
We're working on support for connecting your own materialization storage repository (Redis, database, file system, etc.) to eliminate network calls for sticky assignments and have full control over storage. This feature is currently in development.
153124

154125
---
155126

openfeature-provider/js/src/ConfidenceServerProviderLocal.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ describe('no network', () => {
158158
describe('sticky resolve', () => {
159159
const RESOLVE_REASON_MATCH = 1;
160160

161-
describe('with MaterializationRepository strategy', () => {
161+
// TODO: WIP - MaterializationRepository support
162+
// These tests will be re-enabled when MaterializationRepository is implemented
163+
describe.skip('with MaterializationRepository strategy', () => {
162164
let mockRepository: MockedObject<MaterializationRepository>;
163165
let providerWithRepo: ConfidenceServerProviderLocal;
164166

@@ -353,7 +355,7 @@ describe('sticky resolve', () => {
353355
});
354356
});
355357

356-
describe('without MaterializationRepository (uses RemoteResolverFallback)', () => {
358+
describe('remote resolver fallback for sticky assignments', () => {
357359
let providerWithFallback: ConfidenceServerProviderLocal;
358360

359361
beforeEach(async () => {
@@ -362,7 +364,7 @@ describe('sticky resolve', () => {
362364
apiClientId: 'apiClientId',
363365
apiClientSecret: 'apiClientSecret',
364366
fetch: mockedFetch
365-
// No materializationRepository - will use RemoteResolverFallback
367+
// Uses remote resolver fallback for sticky assignments
366368
});
367369

368370
await providerWithFallback.initialize();
@@ -431,7 +433,7 @@ describe('sticky resolve', () => {
431433
expect(remoteResolveEndpoint).toHaveBeenCalledTimes(1);
432434
});
433435

434-
it('should not store updates when no repository configured', async () => {
436+
it('should handle updates with remote fallback (no local storage)', async () => {
435437
mockedWasmResolver.resolveWithSticky.mockReturnValue({
436438
success: {
437439
response: {
@@ -459,8 +461,8 @@ describe('sticky resolve', () => {
459461
targetingKey: 'user-1'
460462
});
461463

462-
// Should not try to store when no repository is configured
463-
// (success - no exception thrown)
464+
// Updates are handled by remote resolver, not stored locally
465+
// Verify no remote resolve was needed (successful local resolve)
464466
expect(remoteResolveEndpoint).not.toHaveBeenCalled();
465467
});
466468
});

openfeature-provider/js/src/ConfidenceServerProviderLocal.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import {
1919
import { Fetch, FetchMiddleware, withAuth, withLogging, withResponse, withRetry, withRouter, withStallTimeout, withTimeout } from './fetch';
2020
import { scheduleWithFixedInterval, timeoutSignal, TimeUnit } from './util';
2121
import { AccessToken, LocalResolver, ResolveStateUri } from './LocalResolver';
22-
import type { MaterializationRepository } from './MaterializationRepository';
23-
import { handleMissingMaterializations, storeUpdates } from './materializationUtils';
22+
// TODO: WIP - MaterializationRepository support
23+
// import type { MaterializationRepository } from './MaterializationRepository';
24+
// import { handleMissingMaterializations, storeUpdates } from './materializationUtils';
2425

2526
export const DEFAULT_STATE_INTERVAL = 30_000;
2627
export const DEFAULT_FLUSH_INTERVAL = 10_000;
@@ -31,7 +32,6 @@ export interface ProviderOptions {
3132
initializeTimeout?:number,
3233
flushInterval?:number,
3334
fetch?: typeof fetch,
34-
materializationRepository?: MaterializationRepository,
3535
}
3636

3737
/**
@@ -49,7 +49,6 @@ export class ConfidenceServerProviderLocal implements Provider {
4949
private readonly main = new AbortController();
5050
private readonly fetch:Fetch;
5151
private readonly flushInterval:number;
52-
private readonly materializationRepository?: MaterializationRepository;
5352
private stateEtag:string | null = null;
5453

5554

@@ -58,7 +57,6 @@ export class ConfidenceServerProviderLocal implements Provider {
5857
// TODO better error handling
5958
// TODO validate options
6059
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
61-
this.materializationRepository = options.materializationRepository;
6260
const withConfidenceAuth = withAuth(async () => {
6361
const { accessToken, expiresIn } = await this.fetchToken();
6462
return [accessToken, new Date(Date.now() + 1000*expiresIn)]
@@ -130,15 +128,14 @@ export class ConfidenceServerProviderLocal implements Provider {
130128
async onClose(): Promise<void> {
131129
this.main.abort();
132130
await this.flush();
133-
await this.materializationRepository?.close();
134131
}
135132

136133
// TODO test unknown flagClientSecret
137134
async evaluate<T>(flagKey: string, defaultValue: T, context: EvaluationContext): Promise<ResolutionDetails<T>> {
138135
const [flagName, ...path] = flagKey.split('.');
139136

140137
// Build resolve request
141-
// Always use sticky resolve request
138+
// Always use sticky resolve request with remote fallback
142139
const stickyRequest: ResolveWithStickyRequest = {
143140
resolveRequest: {
144141
flags: [`flags/${flagName}`],
@@ -147,7 +144,7 @@ export class ConfidenceServerProviderLocal implements Provider {
147144
clientSecret: this.options.flagClientSecret
148145
},
149146
materializationsPerUnit: {},
150-
failFastOnSticky: !this.materializationRepository
147+
failFastOnSticky: true // Always fail fast - use remote resolver for sticky assignments
151148
};
152149

153150
const response = await this.resolveWithStickyInternal(stickyRequest);
@@ -156,7 +153,7 @@ export class ConfidenceServerProviderLocal implements Provider {
156153

157154
/**
158155
* Internal recursive method for resolving with sticky assignments.
159-
*
156+
*
160157
* @private
161158
*/
162159
private async resolveWithStickyInternal(
@@ -167,11 +164,12 @@ export class ConfidenceServerProviderLocal implements Provider {
167164
if (response.success && response.success.response) {
168165
const { response: flagsResponse, updates } = response.success;
169166

167+
// TODO: WIP - MaterializationRepository support
170168
// Store updates if present (only for MaterializationRepository)
171169
// Fire-and-forget - doesn't block resolve path
172-
if (updates.length > 0 && this.materializationRepository) {
173-
storeUpdates(updates, this.materializationRepository);
174-
}
170+
// if (updates.length > 0 && this.materializationRepository) {
171+
// storeUpdates(updates, this.materializationRepository);
172+
// }
175173

176174
return flagsResponse;
177175
}
@@ -180,18 +178,21 @@ export class ConfidenceServerProviderLocal implements Provider {
180178
if (response.missingMaterializations) {
181179
const { items } = response.missingMaterializations;
182180

183-
// If we don't have a MaterializationRepository, use the remote resolver fallback
184-
if (!this.materializationRepository) {
185-
return await this.remoteResolve(request.resolveRequest!);
186-
}
187-
188-
// Handle MaterializationRepository case
189-
const updatedRequest = await handleMissingMaterializations(
190-
request,
191-
items,
192-
this.materializationRepository
193-
);
194-
return this.resolveWithStickyInternal(updatedRequest);
181+
// Use remote resolver fallback for sticky assignments
182+
// Materializations are stored on Confidence servers with 90-day TTL
183+
return await this.remoteResolve(request.resolveRequest!);
184+
185+
// TODO: WIP - MaterializationRepository support
186+
// When MaterializationRepository is implemented, the logic will be:
187+
// if (!this.materializationRepository) {
188+
// return await this.remoteResolve(request.resolveRequest!);
189+
// }
190+
// const updatedRequest = await handleMissingMaterializations(
191+
// request,
192+
// items,
193+
// this.materializationRepository
194+
// );
195+
// return this.resolveWithStickyInternal(updatedRequest);
195196
}
196197

197198
throw new Error('Invalid response: resolve result not set');

openfeature-provider/js/src/MaterializationRepository.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
// ============================================================================
2+
// WIP: MaterializationRepository - Custom Storage for Sticky Assignments
3+
// ============================================================================
4+
// This interface is currently in development and not yet part of the public API.
5+
// When complete, it will allow users to provide their own storage backend
6+
// (Redis, database, file system, etc.) for sticky assignment materializations.
7+
// ============================================================================
8+
19
import type {
210
MaterializationInfo,
311
} from './proto/api';
412

513

614
/**
7-
* Strategy for storing and loading materialized assignments locally.
15+
* WIP: Strategy for storing and loading materialized assignments locally.
816
*
917
* Use this when you want to:
1018
* - Store assignments in a database, Redis, or other persistent storage

openfeature-provider/js/src/materializationUtils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// ============================================================================
2+
// WIP: Materialization Utilities - Helper functions for MaterializationRepository
3+
// ============================================================================
4+
// These utilities are currently in development and not yet part of the public API.
5+
// They support the MaterializationRepository feature for custom sticky assignment storage.
6+
// ============================================================================
7+
18
import type {
29
MaterializationInfo,
310
MaterializationMap,
@@ -8,7 +15,7 @@ import type {
815
import type { MaterializationRepository } from './MaterializationRepository';
916

1017
/**
11-
* Handle missing materializations by loading from repository and building updated request.
18+
* WIP: Handle missing materializations by loading from repository and building updated request.
1219
* Matches Java implementation logic.
1320
*/
1421
export async function handleMissingMaterializations(

0 commit comments

Comments
 (0)