Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ferry): add evict operation to isolate client #621

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/ferry/lib/ferry_isolate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,29 @@ class IsolateClient extends TypedLink {
return super.dispose();
}

/// returns all top-level keys in the cache
Future<Iterable<String>> getCacheKeys() {
return _handleSingleResponseCommand(
(sendPort) => CacheKeysCommand(sendPort));
}

/// evicts to top level selections from the cache
/// e.g. a query like
/// ```graphql
/// query GetPerson {
/// person(id: "1") {
/// id
/// name
/// }
/// }
/// ```
/// would evict the field `Query`->`person({id:"1"})`
/// Consider calling gcCache() after this to remove orphaned data
Future<void> evictOperation(OperationRequest request) {
return _handleSingleResponseCommand(
(sendPort) => EvictOperationCommand(sendPort, request));
}

/// adds a request to the requestController of the client on the isolate
/// this is useful for re-fetch and pagination
/// see https://ferry.gql-dart.dev/docs/pagination
Expand Down
40 changes: 40 additions & 0 deletions packages/ferry/lib/src/isolate/isolate_commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,43 @@ class ClearOptimisticPatchesCommand extends IsolateCommand {
sendPort.send(null);
}
}

@internal
class CacheKeysCommand extends IsolateCommand {
CacheKeysCommand(SendPort sendPort) : super(sendPort);

@override
void handle(
TypedLinkWithCacheAndRequestController link, ReceivePort receivePort) {
final keys = link.cache.store.keys;
sendPort.send(keys);
}
}

@internal
class IdentifyCommand<T> extends IsolateCommand {
final T object;

IdentifyCommand(SendPort sendPort, this.object) : super(sendPort);

@override
void handle(
TypedLinkWithCacheAndRequestController link, ReceivePort receivePort) {
final id = link.cache.identify(object);
sendPort.send(id);
}
}

@internal
class EvictOperationCommand<TData, TVars> extends IsolateCommand {
final OperationRequest<TData, TVars> request;

EvictOperationCommand(SendPort sendPort, this.request) : super(sendPort);

@override
void handle(
TypedLinkWithCacheAndRequestController link, ReceivePort receivePort) {
link.cache.evictOperation(request);
sendPort.send(null);
}
}
69 changes: 49 additions & 20 deletions packages/ferry_cache/lib/src/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,9 @@ class Cache {
}
}

void _evictField(
String entityId,
String fieldName,
Map<String, dynamic> args,
OperationRequest? optimisticRequest,
) {
void _evictField(String entityId, String fieldName, Map<String, dynamic> args,
OperationRequest? optimisticRequest,
[bool eraseCompletely = false]) {
if (optimisticRequest != null) {
/// Set field to `null` in optimistic patch
optimisticPatchesStream.add({
Expand Down Expand Up @@ -349,25 +346,57 @@ class Cache {

final entity = store.get(entityId);
if (entity != null) {
store.put(
entityId,
entity.map(
// NOTE: we need to set to null rather than removing altogether
// to ensure that denormalize doesn't throw a [PartialDataException]
(key, value) => _fieldMatch(
key,
fieldName,
args,
)
? MapEntry(key, null)
: MapEntry(key, value),
),
);
if (eraseCompletely) {
store.put(
entityId,
{
for (final key in entity.keys)
if (!_fieldMatch(key, fieldName, args)) key: entity[key],
},
);
} else {
store.put(
entityId,
entity.map(
// NOTE: we need to set to null rather than removing altogether
// to ensure that denormalize doesn't throw a [PartialDataException]
(key, value) => _fieldMatch(
key,
fieldName,
args,
)
? MapEntry(key, null)
: MapEntry(key, value),
),
);
}
}
_eventStream.add(null);
}
}

/// Evicts all top-level fields from that operation from the cache.
/// Consider calling after this gc() to completely remove orphaned entities.
void evictOperation<TData, TVars>(OperationRequest<TData, TVars> request) {
final operationDefinition = utils.getOperationDefinition(
request.operation.document, request.operation.operationName);
final rootTypeName =
utils.resolveRootTypename(operationDefinition, typePolicies);

final fields = utils.operationFieldNames(
request.operation.document,
request.operation.operationName,
request.varsToJson(),
typePolicies,
possibleTypes,
);

for (final field in fields) {
final fieldKey = utils.FieldKey.parse(field);
_evictField(rootTypeName, fieldKey.fieldName, fieldKey.args, null, true);
}
}

bool _fieldMatch(
String keyString,
String fieldName,
Expand Down
2 changes: 1 addition & 1 deletion packages/ferry_cache/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dependencies:
pedantic: ^1.11.0
dev_dependencies:
test: ^1.16.8
ferry_test_graphql2: ^0.4.0
ferry_test_graphql2:
gql_exec: ^1.0.0
gql: ^1.0.0
gql_tristate_value: ^1.0.0
47 changes: 47 additions & 0 deletions packages/ferry_cache/test/eviction_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ void main() {
hanReq.rebuild((b) => b..vars.friendsAfter = 'chewie'),
hanData,
);

final entityId = cache.identify(hanData.human)!;
final keyLuke =
FieldKey.from('friendsConnection', {'first': 10, 'after': 'luke'});
Expand Down Expand Up @@ -170,4 +171,50 @@ void main() {
expect(cache.store.get('Human:chewie'), isNull);
});
});

group('evictOperation', () {
test('can evict Operation', () {
final cache = Cache();
addTearDown(() {
cache.dispose();
});
cache.writeQuery(
hanReq,
hanData,
);

cache.evictOperation(hanReq);

expect(cache.readQuery(hanReq), equals(null));

final gcResult = cache.gc();

expect(gcResult, equals({'Human:luke', 'Human:chewie', 'Human:han'}));

expect(cache.store.get('Query'), equals({'__typename': 'Query'}));

expect(cache.store.keys, equals({'Query'}));
});

test('only evicts given operation', () {
final cache = Cache();
addTearDown(() {
cache.dispose();
});
cache.writeQuery(
hanReq,
hanData,
);

cache.writeQuery(
chewieReq,
chewieData,
);

cache.evictOperation(hanReq);

expect(cache.readQuery(hanReq), equals(null));
expect(cache.readQuery(chewieReq), equals(chewieData));
});
});
}
Loading