Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix(graphcache): Fix deadlocked layers between deferred and optimistic layers (#2861)

Changed files
+30 -55
.changeset
exchanges
+5
.changeset/poor-items-shake.md
···
+
---
+
'@urql/exchange-graphcache': patch
+
---
+
+
Fix a deadlock condition in Graphcache's layers, which is caused by subscriptions (or other deferred layers) starting before one-off mutation layers. This causes the mutation to not be completed, which keeps its data preferred above the deferred layer. That in turn means that layers stop squashing, which causes new results to be missing indefinitely, when they overlap.
-23
exchanges/graphcache/src/cacheExchange.test.ts
···
variables: undefined,
});
-
const queryOpB = client.createRequestOperation('query', {
-
key: 3,
-
query,
-
variables: undefined,
-
});
-
expect(data).toBe(undefined);
nextOp(queryOpA);
···
nextOp(mutationOp);
expect(reexec).toHaveBeenCalledTimes(1);
expect(data).toHaveProperty('node.name', 'optimistic');
-
-
// NOTE: We purposefully skip the following:
-
// nextOp(queryOpB);
-
-
nextRes({
-
operation: queryOpB,
-
data: {
-
__typename: 'Query',
-
node: {
-
__typename: 'Node',
-
id: 'node',
-
name: 'query b',
-
},
-
},
-
});
-
-
expect(data).toHaveProperty('node.name', 'query b');
});
it('applies mutation results on top of commutative queries', () => {
+2 -5
exchanges/graphcache/src/cacheExchange.ts
···
optimisticKeysToDependencies.delete(operation.key);
}
-
reserveLayer(
-
store.data,
-
operation.key,
-
operation.kind === 'subscription' || result.hasNext
-
);
+
if (operation.kind === 'subscription' || result.hasNext)
+
reserveLayer(store.data, operation.key, true);
let queryDependencies: void | Dependencies;
let data: Data | null = result.data;
+23 -27
exchanges/graphcache/src/store/data.ts
···
while (
--i >= 0 &&
data.refLock.has(data.optimisticOrder[i]) &&
-
data.commutativeKeys.has(data.optimisticOrder[i]) &&
-
!data.deferredKeys.has(data.optimisticOrder[i])
-
) {
+
data.commutativeKeys.has(data.optimisticOrder[i])
+
)
squashLayer(data.optimisticOrder[i]);
-
}
}
currentOwnership = null;
···
layerKey: number,
hasNext?: boolean
) => {
+
// Find the current index for the layer, and remove it from
+
// the order if it exists already
+
let index = data.optimisticOrder.indexOf(layerKey);
+
if (index > -1) data.optimisticOrder.splice(index, 1);
+
if (hasNext) {
data.deferredKeys.add(layerKey);
+
// If the layer has future results then we'll move it past any layer that's
+
// still empty, so currently pending operations will take precedence over it
+
for (
+
index = index > -1 ? index : 0;
+
index < data.optimisticOrder.length &&
+
!data.deferredKeys.has(data.optimisticOrder[index]) &&
+
(!data.refLock.has(data.optimisticOrder[index]) ||
+
!data.commutativeKeys.has(data.optimisticOrder[index]));
+
index++
+
);
} else {
data.deferredKeys.delete(layerKey);
-
}
-
-
let index = data.optimisticOrder.indexOf(layerKey);
-
if (index > -1) {
-
if (!data.commutativeKeys.has(layerKey) && !hasNext) {
-
data.optimisticOrder.splice(index, 1);
-
// Protect optimistic layers from being turned into non-optimistic layers
-
// while preserving optimistic data
+
// Protect optimistic layers from being turned into non-optimistic layers
+
// while preserving optimistic data
+
if (index > -1 && !data.commutativeKeys.has(layerKey))
clearLayer(data, layerKey);
-
} else {
-
return;
-
}
-
}
-
-
// If the layer has future results then we'll move it past any layer that's
-
// still empty, so currently pending operations will take precedence over it
-
for (
index = 0;
-
hasNext &&
-
index < data.optimisticOrder.length &&
-
!data.deferredKeys.has(data.optimisticOrder[index]) &&
-
(!data.refLock.has(data.optimisticOrder[index]) ||
-
!data.commutativeKeys.has(data.optimisticOrder[index]));
-
index++
-
);
+
}
+
// Register the layer with the deferred or "top" index and
+
// mark it as commutative
data.optimisticOrder.splice(index, 0, layerKey);
data.commutativeKeys.add(layerKey);
};