Skip to content

Commit edf2e48

Browse files
vladarvrazuvaev
andauthored
fix(apollo-forest-run): index covered operations (#661)
* fix(apollo-forest-run): index covered operations * add benchmark * Change files --------- Co-authored-by: vrazuvaev <vrazuvaev@microsoft.com_msteamsmdb>
1 parent 07fc20b commit edf2e48

7 files changed

Lines changed: 67 additions & 8 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(apollo-forest-run): index covered operations",
4+
"packageName": "@graphitation/apollo-forest-run",
5+
"email": "vrazuvaev@microsoft.com_msteamsmdb",
6+
"dependentChangeType": "patch"
7+
}

packages/apollo-forest-run-benchmark/src/scenarios.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import type { ForestRun } from "@graphitation/apollo-forest-run";
22
import type { Scenario, ScenarioContext } from "./types";
3+
import { parse } from "graphql";
4+
5+
// Pre-parse 50 unique named queries to simulate a real app with many active operations.
6+
// Each has its own operation name and data, creating separate trees in the forest.
7+
// This is the setup that exposes O(n) scans in getCoveringOperationIds.
8+
const EXTRA_OPS_COUNT = 50;
9+
const extraOperations = Array.from({ length: EXTRA_OPS_COUNT }, (_, i) => ({
10+
query: parse(`query BgQuery${i} { node${i}(id: "${i}") { id value } }`),
11+
data: { [`node${i}`]: { __typename: `Node${i}`, id: `${i}`, value: i } },
12+
}));
313

414
const addWatchers = (
515
watcherCount: number,
@@ -404,4 +414,32 @@ export const scenarios = [
404414
};
405415
},
406416
},
417+
{
418+
name: "write-many-ops-one-field-change",
419+
prepare: (ctx: ScenarioContext) => {
420+
const { operations, CacheFactory, configuration, watcherCount } = ctx;
421+
const cache = new CacheFactory(configuration);
422+
423+
// Populate the cache with many unrelated operations (simulating real app)
424+
for (const { query, data } of extraOperations) {
425+
cache.writeQuery({ query, data });
426+
}
427+
428+
// Write the main query, add watchers, then measure a write with a change.
429+
// Each watcher's diff triggers readOperation → applyTransformations →
430+
// createChunkMatcher → getCoveringOperationIds which scans all trees.
431+
const { data, query } = operations["complex-nested"];
432+
cache.writeQuery({ query, data: data["complex-nested"] });
433+
addWatchers(watcherCount, cache, query);
434+
435+
return {
436+
run() {
437+
return cache.writeQuery({
438+
query,
439+
data: data["complex-nested-single-field-change"],
440+
});
441+
},
442+
};
443+
},
444+
},
407445
] as const satisfies Scenario[];

packages/apollo-forest-run/src/__tests__/helpers/forest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function createTestForest(): IndexedForest {
3636
operationsByNodes: new Map(),
3737
operationsWithErrors: new Set(),
3838
operationsByName: new Map(),
39+
operationsByCoveredName: new Map(),
3940
operationsByPartitions: new Map(),
4041
deletedNodes: new Set(),
4142
};

packages/apollo-forest-run/src/cache/draftHelpers.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,15 @@ function getCoveringOperationIds(
248248
let ids: Set<OperationId> | undefined;
249249

250250
for (const layer of layers) {
251-
// Forward: find ops that cover us (their covers list includes our name)
251+
// Forward: find ops that cover us via pre-built index
252252
if (opName) {
253-
for (const tree of layer.trees.values()) {
254-
if (
255-
tree.operation.id !== operation.id &&
256-
tree.operation.covers.includes(opName)
257-
) {
258-
if (!ids) ids = new Set();
259-
ids.add(tree.operation.id);
253+
const coveringIds = layer.operationsByCoveredName.get(opName);
254+
if (coveringIds) {
255+
for (const id of coveringIds) {
256+
if (id !== operation.id) {
257+
if (!ids) ids = new Set();
258+
ids.add(id);
259+
}
260260
}
261261
}
262262
}

packages/apollo-forest-run/src/cache/store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function createStore(_: CacheEnv): Store {
2727
trees: new Map(),
2828
operationsByNodes: new Map<NodeKey, Set<OperationId>>(),
2929
operationsByName: new Map(),
30+
operationsByCoveredName: new Map(),
3031
operationsByPartitions: new Map(),
3132
operationsWithErrors: new Set<OperationDescriptor>(),
3233
extraRootIds: new Map<NodeKey, TypeName>(),
@@ -193,6 +194,7 @@ export function createOptimisticLayer(
193194
operationsByNodes: new Map(),
194195
operationsWithErrors: new Set(),
195196
operationsByName: new Map(),
197+
operationsByCoveredName: new Map(),
196198
operationsByPartitions: new Map(),
197199
extraRootIds: new Map(),
198200
readResults: new Map(),
@@ -361,6 +363,9 @@ function removeDataTree(
361363
dataForest.readResults.delete(operation);
362364
dataForest.operationsWithErrors.delete(operation);
363365
dataForest.operationsByName.get(operation.name ?? "")?.delete(operation.id);
366+
for (const coveredName of operation.covers) {
367+
dataForest.operationsByCoveredName.get(coveredName)?.delete(operation.id);
368+
}
364369
dataForest.operationsByPartitions.get(partition)?.delete(operation.id);
365370
optimisticReadResults.delete(operation);
366371
partialReadResults.delete(operation);
@@ -380,6 +385,7 @@ export function resetStore(store: Store): void {
380385
dataForest.operationsByNodes.clear();
381386
dataForest.operationsWithErrors.clear();
382387
dataForest.operationsByName.clear();
388+
dataForest.operationsByCoveredName.clear();
383389
dataForest.operationsWithDanglingRefs.clear();
384390
dataForest.readResults.clear();
385391
operations.clear();

packages/apollo-forest-run/src/forest/addTree.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ function trackOperationName(forest: IndexedForest, tree: IndexedTree) {
5050
const name = tree.operation.name;
5151
if (!name) return;
5252
getOrCreate(forest.operationsByName, name, newSet).add(tree.operation.id);
53+
54+
for (const coveredName of tree.operation.covers) {
55+
getOrCreate(forest.operationsByCoveredName, coveredName, newSet).add(
56+
tree.operation.id,
57+
);
58+
}
5359
}
5460

5561
function trackPartitions(

packages/apollo-forest-run/src/forest/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export type IndexedForest = {
125125
operationsByNodes: Map<NodeKey, Set<OperationId>>; // May contain false positives
126126
operationsWithErrors: Set<OperationDescriptor>; // May contain false positives
127127
operationsByName: Map<string, Set<OperationId>>; // operationName → operation IDs
128+
operationsByCoveredName: Map<string, Set<OperationId>>; // coveredName → IDs of ops whose covers list includes it
128129
operationsByPartitions: Map<string, Set<OperationId>>; // partition key => operation IDs
129130
deletedNodes: Set<NodeKey>;
130131
};

0 commit comments

Comments
 (0)