Skip to content

Commit 98b3652

Browse files
committed
Don't corrupt type registry with false abstract-type tag under deferDeduplicatedFields
RelayResponseNormalizer's TypeDiscriminator and InlineFragment normalization paths unconditionally write 'client:__type:<ConcreteType>.__is<Interface>: <hasOwnProperty>' during payload traversal. Without deferDeduplicatedFields, this is fine: a missing __isFoo tag in a normalized payload is a real 'type does not implement Foo' signal, and the false write is load-bearing so DataChecker can skip future '... on Foo' selections for that concrete type. Under Environment({ deferDeduplicatedFields: true }), the same missing tag can just mean 'delivered earlier in a different chunk of this operation, dedup'd out of the current chunk'. Writing false in that case is wrong: it plants a schema-contradicting negative claim in the type registry, and DataChecker (which treats false as authoritative non-implementation) then silently skips '... on <Interface>' subtrees for every future query touching that concrete type — never marking anything missing, never fetching. Symptom in our app: after an SSR page load that fetches a defer'd query, client-navigations to any page that reads a Cloudcast via 'node(id) { ... on Node { ... on Cloudcast ... } }' rendered null for every field inside — reader saw $isWithinUnmatchedTypeRefinement:true because DataChecker had marked the query 'available' without ever fetching, thanks to __type:Cloudcast.__isNode=false written during the defer'd chunk. Fix: skip the write when 'hasOwnProperty === false && this ._deferDeduplicatedFields'. When the write is skipped, DataChecker treats the registry entry as unknown (_implementsInterface == null) → handles missing → fetches on next check → the response arrives with the tag → true written normally. Non-dedup environments are byte-identical to the previous behaviour. Filed as #5389.
1 parent 5bd5328 commit 98b3652

1 file changed

Lines changed: 31 additions & 18 deletions

File tree

packages/relay-runtime/store/RelayResponseNormalizer.js

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -283,18 +283,24 @@ class RelayResponseNormalizer {
283283
data,
284284
abstractKey,
285285
);
286-
const typeName = RelayModernRecord.getType(record);
287-
const typeID = generateTypeID(typeName);
288-
let typeRecord = this._recordSource.get(typeID);
289-
if (typeRecord == null) {
290-
typeRecord = RelayModernRecord.create(typeID, TYPE_SCHEMA_TYPE);
291-
this._recordSource.set(typeID, typeRecord);
286+
// Under `deferDeduplicatedFields` a missing abstract typename means
287+
// the server omitted it as already-delivered, not that the concrete
288+
// type fails the type refinement. Skip the write; DataChecker then
289+
// treats the entry as unknown and refetches on the next check.
290+
if (implementsInterface || !this._deferDeduplicatedFields) {
291+
const typeName = RelayModernRecord.getType(record);
292+
const typeID = generateTypeID(typeName);
293+
let typeRecord = this._recordSource.get(typeID);
294+
if (typeRecord == null) {
295+
typeRecord = RelayModernRecord.create(typeID, TYPE_SCHEMA_TYPE);
296+
this._recordSource.set(typeID, typeRecord);
297+
}
298+
RelayModernRecord.setValue(
299+
typeRecord,
300+
abstractKey,
301+
implementsInterface,
302+
);
292303
}
293-
RelayModernRecord.setValue(
294-
typeRecord,
295-
abstractKey,
296-
implementsInterface,
297-
);
298304
break;
299305
}
300306
case 'LinkedHandle':
@@ -402,14 +408,21 @@ class RelayResponseNormalizer {
402408
data,
403409
abstractKey,
404410
);
405-
const typeName = RelayModernRecord.getType(record);
406-
const typeID = generateTypeID(typeName);
407-
let typeRecord = this._recordSource.get(typeID);
408-
if (typeRecord == null) {
409-
typeRecord = RelayModernRecord.create(typeID, TYPE_SCHEMA_TYPE);
410-
this._recordSource.set(typeID, typeRecord);
411+
// See the `TypeDiscriminator` case above for the reasoning.
412+
if (implementsInterface || !this._deferDeduplicatedFields) {
413+
const typeName = RelayModernRecord.getType(record);
414+
const typeID = generateTypeID(typeName);
415+
let typeRecord = this._recordSource.get(typeID);
416+
if (typeRecord == null) {
417+
typeRecord = RelayModernRecord.create(typeID, TYPE_SCHEMA_TYPE);
418+
this._recordSource.set(typeID, typeRecord);
419+
}
420+
RelayModernRecord.setValue(
421+
typeRecord,
422+
abstractKey,
423+
implementsInterface,
424+
);
411425
}
412-
RelayModernRecord.setValue(typeRecord, abstractKey, implementsInterface);
413426
if (implementsInterface) {
414427
this._traverseSelections(selection, record, data);
415428
}

0 commit comments

Comments
 (0)