Skip to content

Commit 1ee2fe3

Browse files
committed
chore: love repo up one level
Signed-off-by: Jeremy Andrews <[email protected]>
1 parent b873f8e commit 1ee2fe3

31 files changed

+3943
-0
lines changed

Diff for: CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
3+
## [0.0.2](https://github.com/open-feature/dart-server-sdk/compare/open_feature_dart_server_sdk-v0.0.1...open_feature_dart_server_sdk-v0.0.2) (2025-03-14)
4+
5+
6+
### ✨ New Features
7+
8+
* release v.0.0.1-pre+1 ([#25](https://github.com/open-feature/dart-server-sdk/issues/25)) ([83b6438](https://github.com/open-feature/dart-server-sdk/commit/83b643864d7d6e100cfc337e2abf05eadd8f241e))

Diff for: lib/client.dart

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import 'dart:async';
2+
import 'package:logging/logging.dart';
3+
import 'evaluation_context.dart';
4+
import 'hooks.dart';
5+
import 'feature_provider.dart';
6+
import 'transaction_context.dart';
7+
8+
class CacheEntry<T> {
9+
final T value;
10+
final DateTime expiresAt;
11+
final String contextHash;
12+
13+
CacheEntry({
14+
required this.value,
15+
required Duration ttl,
16+
required this.contextHash,
17+
}) : expiresAt = DateTime.now().add(ttl);
18+
19+
bool get isExpired => DateTime.now().isAfter(expiresAt);
20+
}
21+
22+
class ClientMetadata {
23+
final String name;
24+
final String version;
25+
final Map<String, String> attributes;
26+
27+
ClientMetadata({
28+
required this.name,
29+
this.version = '1.0.0',
30+
this.attributes = const {},
31+
});
32+
}
33+
34+
class ClientMetrics {
35+
int flagEvaluations = 0;
36+
int cacheHits = 0;
37+
int cacheMisses = 0;
38+
List<Duration> responseTimes = [];
39+
Map<String, int> errorCounts = {};
40+
41+
Duration get averageResponseTime {
42+
if (responseTimes.isEmpty) return Duration.zero;
43+
final total = responseTimes.fold<int>(
44+
0, (sum, duration) => sum + duration.inMilliseconds);
45+
return Duration(milliseconds: total ~/ responseTimes.length);
46+
}
47+
48+
Map<String, dynamic> toJson() => {
49+
'flagEvaluations': flagEvaluations,
50+
'cacheHits': cacheHits,
51+
'cacheMisses': cacheMisses,
52+
'averageResponseTime': averageResponseTime.inMilliseconds,
53+
'errorCounts': errorCounts,
54+
};
55+
}
56+
57+
class FeatureClient {
58+
final Logger _logger = Logger('FeatureClient');
59+
final ClientMetadata metadata;
60+
final HookManager _hookManager;
61+
final EvaluationContext _defaultContext;
62+
final FeatureProvider _provider;
63+
final TransactionContextManager _transactionManager;
64+
final ClientMetrics _metrics = ClientMetrics();
65+
66+
final Duration _cacheTtl;
67+
final int _maxCacheSize;
68+
final Map<String, CacheEntry<dynamic>> _cache = {};
69+
70+
FeatureClient({
71+
required this.metadata,
72+
required HookManager hookManager,
73+
required EvaluationContext defaultContext,
74+
FeatureProvider? provider,
75+
TransactionContextManager? transactionManager,
76+
Duration cacheTtl = const Duration(minutes: 5),
77+
int maxCacheSize = 1000,
78+
}) : _hookManager = hookManager,
79+
_defaultContext = defaultContext,
80+
_provider = provider ?? InMemoryProvider({}),
81+
_transactionManager = transactionManager ?? TransactionContextManager(),
82+
_cacheTtl = cacheTtl,
83+
_maxCacheSize = maxCacheSize;
84+
85+
String _generateCacheKey(String flagKey, Map<String, dynamic>? context) {
86+
final buffer = StringBuffer(flagKey);
87+
if (context != null) {
88+
buffer.write(context.toString());
89+
}
90+
return buffer.toString();
91+
}
92+
93+
void _addToCache<T>(String key, T value, String contextHash) {
94+
if (_cache.length >= _maxCacheSize) {
95+
_cache.remove(_cache.keys.first);
96+
}
97+
_cache[key] = CacheEntry<T>(
98+
value: value,
99+
ttl: _cacheTtl,
100+
contextHash: contextHash,
101+
);
102+
}
103+
104+
T? _getFromCache<T>(String key, String contextHash) {
105+
final entry = _cache[key];
106+
if (entry == null || entry.isExpired || entry.contextHash != contextHash) {
107+
_metrics.cacheMisses++;
108+
_cache.remove(key);
109+
return null;
110+
}
111+
_metrics.cacheHits++;
112+
return entry.value as T;
113+
}
114+
115+
Future<T> _evaluateFlag<T>(
116+
String flagKey,
117+
T defaultValue,
118+
Future<FlagEvaluationResult<T>> Function(Map<String, dynamic>?) evaluator, {
119+
Map<String, dynamic>? context,
120+
}) async {
121+
final startTime = DateTime.now();
122+
_metrics.flagEvaluations++;
123+
124+
try {
125+
final effectiveContext = {
126+
..._defaultContext.attributes,
127+
...context ?? {},
128+
..._transactionManager.currentContext?.effectiveAttributes ?? {},
129+
};
130+
131+
final cacheKey = _generateCacheKey(flagKey, effectiveContext);
132+
final contextHash = effectiveContext.toString();
133+
134+
final cachedValue = _getFromCache<T>(cacheKey, contextHash);
135+
if (cachedValue != null) {
136+
return cachedValue;
137+
}
138+
139+
await _hookManager.executeHooks(
140+
HookStage.BEFORE,
141+
flagKey,
142+
effectiveContext,
143+
);
144+
145+
final result = await evaluator(effectiveContext);
146+
147+
await _hookManager.executeHooks(
148+
HookStage.AFTER,
149+
flagKey,
150+
effectiveContext,
151+
result: result,
152+
);
153+
154+
_addToCache(cacheKey, result.value, contextHash);
155+
156+
_metrics.responseTimes.add(DateTime.now().difference(startTime));
157+
return result.value;
158+
} catch (e) {
159+
_logger.warning('Error evaluating flag $flagKey: $e');
160+
_metrics.errorCounts[e.runtimeType.toString()] =
161+
(_metrics.errorCounts[e.runtimeType.toString()] ?? 0) + 1;
162+
163+
await _hookManager.executeHooks(
164+
HookStage.ERROR,
165+
flagKey,
166+
context,
167+
error: e is Exception ? e : Exception(e.toString()),
168+
);
169+
return defaultValue;
170+
} finally {
171+
await _hookManager.executeHooks(
172+
HookStage.FINALLY,
173+
flagKey,
174+
context,
175+
);
176+
}
177+
}
178+
179+
Future<bool> getBooleanFlag(
180+
String flagKey, {
181+
EvaluationContext? context,
182+
bool defaultValue = false,
183+
}) =>
184+
_evaluateFlag(
185+
flagKey,
186+
defaultValue,
187+
(ctx) => _provider.getBooleanFlag(flagKey, defaultValue, context: ctx),
188+
context: context?.attributes,
189+
);
190+
191+
Future<String> getStringFlag(
192+
String flagKey, {
193+
EvaluationContext? context,
194+
String defaultValue = '',
195+
}) =>
196+
_evaluateFlag(
197+
flagKey,
198+
defaultValue,
199+
(ctx) => _provider.getStringFlag(flagKey, defaultValue, context: ctx),
200+
context: context?.attributes,
201+
);
202+
203+
Future<int> getIntegerFlag(
204+
String flagKey, {
205+
EvaluationContext? context,
206+
int defaultValue = 0,
207+
}) =>
208+
_evaluateFlag(
209+
flagKey,
210+
defaultValue,
211+
(ctx) => _provider.getIntegerFlag(flagKey, defaultValue, context: ctx),
212+
context: context?.attributes,
213+
);
214+
215+
Future<double> getDoubleFlag(
216+
String flagKey, {
217+
EvaluationContext? context,
218+
double defaultValue = 0.0,
219+
}) =>
220+
_evaluateFlag(
221+
flagKey,
222+
defaultValue,
223+
(ctx) => _provider.getDoubleFlag(flagKey, defaultValue, context: ctx),
224+
context: context?.attributes,
225+
);
226+
227+
Future<Map<String, dynamic>> getObjectFlag(
228+
String flagKey, {
229+
EvaluationContext? context,
230+
Map<String, dynamic> defaultValue = const {},
231+
}) =>
232+
_evaluateFlag(
233+
flagKey,
234+
defaultValue,
235+
(ctx) => _provider.getObjectFlag(flagKey, defaultValue, context: ctx),
236+
context: context?.attributes,
237+
);
238+
239+
ClientMetrics getMetrics() => _metrics;
240+
241+
void clearCache() => _cache.clear();
242+
}

Diff for: lib/domain.dart

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
class DomainConfiguration {
2+
final String name;
3+
final Map<String, dynamic> settings;
4+
final String? parentDomain;
5+
final List<String> childDomains;
6+
7+
DomainConfiguration({
8+
required this.name,
9+
this.settings = const {},
10+
this.parentDomain,
11+
this.childDomains = const [],
12+
});
13+
14+
bool validate() {
15+
if (name.isEmpty) return false;
16+
if (parentDomain?.isEmpty ?? false) return false;
17+
return true;
18+
}
19+
}
20+
21+
class Domain {
22+
final String clientId;
23+
final String providerName;
24+
final DomainConfiguration config;
25+
final Domain? parent;
26+
final List<Domain> children = [];
27+
28+
Domain(
29+
this.clientId,
30+
this.providerName, {
31+
required this.config,
32+
this.parent,
33+
}) {
34+
if (parent != null) {
35+
parent!.children.add(this);
36+
}
37+
}
38+
39+
Map<String, dynamic> get effectiveSettings {
40+
final parentSettings = parent?.effectiveSettings ?? {};
41+
return {
42+
...parentSettings,
43+
...config.settings,
44+
};
45+
}
46+
47+
bool isChildOf(Domain other) {
48+
var current = parent;
49+
while (current != null) {
50+
if (current == other) return true;
51+
current = current.parent;
52+
}
53+
return false;
54+
}
55+
}

0 commit comments

Comments
 (0)