Skip to content

Commit

Permalink
feat: Prevent excessive fetch calls
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Dec 18, 2024
2 parents 487fba8 + 9bc0717 commit 8f8dd2f
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.6.0

* Feat: save network calls when context fields don't change

## 1.5.3

* Fix: payload stringify in bootstrap
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final unleash = UnleashClient(
url: Uri.parse('https://<your-unleash-instance>/api/frontend'),
clientKey: '<your-client-side-token>',
appName: 'my-app');
unleash.start();
```

#### Connection options
Expand Down
28 changes: 28 additions & 0 deletions lib/unleash_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,32 @@ class UnleashContext {

return params;
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! UnleashContext) return false;
return other.userId == userId &&
other.sessionId == sessionId &&
other.remoteAddress == remoteAddress &&
_mapEquals(other.properties, properties);
}

@override
int get hashCode {
return Object.hash(
userId,
sessionId,
remoteAddress,
properties,
);
}

static bool _mapEquals(Map<String, String> map1, Map<String, String> map2) {
if (map1.length != map2.length) return false;
for (final key in map1.keys) {
if (map1[key] != map2[key]) return false;
}
return true;
}
}
21 changes: 21 additions & 0 deletions lib/unleash_proxy_client_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,27 @@ class UnleashClient extends EventEmitter {
}
}

/// Checks if any of the provided context fields are different from the current ones.
bool _anyFieldHasChanged(Map<String, String> fields) {
for (var entry in fields.entries) {
String key = entry.key;
String newValue = entry.value;

if (key == 'userId') {
if (context.userId != newValue) return true;
} else if (key == 'sessionId') {
if (context.sessionId != newValue) return true;
} else if (key == 'remoteAddress') {
if (context.remoteAddress != newValue) return true;
} else {
if (context.properties[key] != newValue) return true;
}
}
return false;
}

Future<void> updateContext(UnleashContext unleashContext) async {
if (unleashContext == context) return;
if (started == false) {
await _waitForEvent('initialized');
_updateContextFields(unleashContext);
Expand Down Expand Up @@ -286,6 +306,7 @@ class UnleashClient extends EventEmitter {
}

Future<void> setContextFields(Map<String, String> fields) async {
if (!_anyFieldHasChanged(fields)) return;
if (clientState == ClientState.ready) {
fields.forEach((field, value) {
_updateContextField(field, value);
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: A Flutter/Dart client that can be used together with the unleash-pr
homepage: https://github.com/Unleash/unleash_proxy_client_flutter
repository: https://github.com/Unleash/unleash_proxy_client_flutter
issue_tracker: https://github.com/Unleash/unleash_proxy_client_flutter
version: 1.5.3
version: 1.6.0

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down
94 changes: 94 additions & 0 deletions test/unleash_proxy_client_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,100 @@ void main() {
]);
});

test(
'set and update context with the same value will not trigger new fetch call',
() async {
final getMock = GetMock();
final unleash = UnleashClient(
url: url,
clientKey: 'proxy-123',
appName: 'flutter-test',
sessionIdGenerator: generateSessionId,
storageProvider: InMemoryStorageProvider(),
fetcher: getMock);
await unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {'customKey': 'customValue'}));
// update whole context before start but keep data identical
await unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {'customKey': 'customValue'}));
// set standard property before start
unleash.setContextField('userId', '123');
// set standard an custom property before start
unleash.setContextFields({'customKey': 'customValue', 'userId': '123'});
await unleash.start();

// set standard properties after start
await unleash.setContextField('userId', '123');
await unleash.setContextField('remoteAddress', 'address');
await unleash.setContextField('sessionId', 'session');
// set custom property after start
await unleash.setContextField('customKey', 'customValue');
// set standard and custom property after start
await unleash
.setContextFields({'customKey': 'customValue', 'userId': '123'});
// update whole context after start
await unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {'customKey': 'customValue'}));

expect(getMock.calledTimes, 1);
expect(getMock.calledWithUrls, [
Uri.parse(
'https://app.unleash-hosted.com/demo/api/proxy?userId=123&remoteAddress=address&sessionId=session&properties%5BcustomKey%5D=customValue&appName=flutter-test&environment=default')
]);
});

test('update context removing fields triggers new flag update', () async {
final getMock = GetMock();
final unleash = UnleashClient(
url: url,
clientKey: 'proxy-123',
appName: 'flutter-test',
sessionIdGenerator: generateSessionId,
storageProvider: InMemoryStorageProvider(),
fetcher: getMock);
// ignore this one
unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {
'customKey': 'customValue',
'remove1': 'val1',
'remove2': 'val2'
}));
// first call
unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {'customKey': 'customValue', 'remove1': 'val1'}));
await unleash.start();

// remove another field and second call
await unleash.updateContext(UnleashContext(
userId: '123',
remoteAddress: 'address',
sessionId: 'session',
properties: {'customKey': 'customValue'}));

expect(getMock.calledTimes, 2);
expect(getMock.calledWithUrls, [
Uri.parse(
'https://app.unleash-hosted.com/demo/api/proxy?userId=123&remoteAddress=address&sessionId=session&properties%5BcustomKey%5D=customValue&properties%5Bremove1%5D=val1&appName=flutter-test&environment=default'),
Uri.parse(
'https://app.unleash-hosted.com/demo/api/proxy?userId=123&remoteAddress=address&sessionId=session&properties%5BcustomKey%5D=customValue&appName=flutter-test&environment=default')
]);
});

test('update context without await', () async {
final getMock = GetMock();
final unleash = UnleashClient(
Expand Down

0 comments on commit 8f8dd2f

Please sign in to comment.